Lua Boomshine Update with circles + levels

added a few more levels and increased difficulty based on level

Rliimage allows us to use circles in boomshine
Optimized some of the code to exceed the speed of the original

Change-Id: I68eb74f8b68fcdc8a9d022cd172374744425f15d
diff --git a/apps/plugins/boomshine.lua b/apps/plugins/boomshine.lua
index 1729cbb..63ac6b1 100644
--- a/apps/plugins/boomshine.lua
+++ b/apps/plugins/boomshine.lua
@@ -11,6 +11,7 @@
  See http://www.yvoschaap.com/chainrxn/ and http://www.k2xl.com/games/boomshine/
 
  Copyright (C) 2009 by Maurus Cuelenaere
+ Copyright (C) 2018 William Wilgus -- Added circles, blit cursor, hard levels
 
  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
@@ -21,14 +22,32 @@
  KIND, either express or implied.
 
 ]]--
-
 require "actions"
+-- [[only save the actions we are using]]
+actions_pla = {}
+for key, val in pairs(rb.actions) do
+    for _, v in ipairs({"PLA_", "TOUCHSCREEN"}) do
+        if string.find (key, v) then
+            actions_pla[key] = val
+            break
+        end
+    end
+end
+rb.actions = nil
+rb.contexts = nil
+-------------------------------------
+
+local _LCD = rb.lcd_framebuffer()
+local BSAND = 0x8
+local rocklib_image = getmetatable(rb.lcd_framebuffer())
+local _ellipse = rocklib_image.ellipse
 
 local CYCLETIME = rb.HZ / 50
 local HAS_TOUCHSCREEN = rb.action_get_touchscreen_press ~= nil
 local DEFAULT_BALL_SIZE = rb.LCD_HEIGHT > rb.LCD_WIDTH and rb.LCD_WIDTH  / 30
                                                        or  rb.LCD_HEIGHT / 30
 local MAX_BALL_SPEED = DEFAULT_BALL_SIZE / 2
+local DEC_BALL_SPEED = DEFAULT_BALL_SIZE / 8
 local DEFAULT_FOREGROUND_COLOR = rb.lcd_get_foreground ~= nil
                                                      and rb.lcd_get_foreground()
                                                      or  0
@@ -46,7 +65,15 @@
                 {30, 45},
                 {37, 50},
                 {48, 55},
-                {55, 60}
+                {59, 60},
+                {29, 30},
+                {24, 25},
+                {19, 20},
+                {14, 15},
+                {9,  10},
+                {10, 10},
+                {4,   5},
+                {5,   5}
            }
 
 local Ball = {
@@ -55,16 +82,44 @@
                 implosion = false
              }
 
-function Ball:new(o)
+local function create_cursor(size)
+    local cursor
+    if not HAS_TOUCHSCREEN then
+        cursor = rb.new_image(size, size)
+        cursor:clear(0)
+        local sz2 = size / 2
+        local sz4 = size / 4
+
+        cursor:line(1, 1, sz4, 1, 1)
+        cursor:line(1, 1, 1, sz4, 1)
+
+        cursor:line(1, size, sz4, size, 1)
+        cursor:line(1, size, 1, size - sz4, 1)
+
+        cursor:line(size, size, size - sz4, size, 1)
+        cursor:line(size, size, size, size - sz4, 1)
+
+        cursor:line(size, 1, size - sz4, 1, 1)
+        cursor:line(size, 1, size, sz4, 1)
+
+        --crosshairs
+        cursor:line(sz2 - sz4, sz2, sz2 + sz4, sz2, 1)
+        cursor:line(sz2, sz2 - sz4, sz2, sz2 + sz4, 1)
+    end
+    return cursor
+end
+
+function Ball:new(o, level)
+    level = level or 1
     if o == nil then
         o = {
                 x = math.random(0, rb.LCD_WIDTH - self.size),
                 y = math.random(0, rb.LCD_HEIGHT - self.size),
                 color = random_color(),
-                up_speed = Ball:generateSpeed(),
-                right_speed = Ball:generateSpeed(),
+                up_speed = Ball:generateSpeed(level),
+                right_speed = Ball:generateSpeed(level),
                 explosion_size = math.random(2*self.size, 4*self.size),
-                life_duration = math.random(rb.HZ, rb.HZ*5)
+                life_duration = math.random(rb.HZ / level, rb.HZ*5),
             }
     end
 
@@ -73,8 +128,10 @@
     return o
 end
 
-function Ball:generateSpeed()
-    local speed = math.random(-MAX_BALL_SPEED, MAX_BALL_SPEED)
+function Ball:generateSpeed(level)
+    local ballspeed = MAX_BALL_SPEED - (DEC_BALL_SPEED * (level - 1))
+    if ballspeed < 2 then ballspeed = 2 end
+    local speed = math.random(-ballspeed, ballspeed)
     if speed == 0 then
         speed = 1       -- Make sure all balls move
     end
@@ -82,39 +139,48 @@
     return speed
 end
 
+function Ball:draw_exploded()
+    --[[
+    --set_foreground(self.color)
+    --rb.lcd_fillrect(self.x, self.y, self.size, self.size)
+    ]]
+
+    _ellipse(_LCD, self.x, self.y,
+             self.x + self.size, self.y + self.size , self.color, nil, true)
+end
+
 function Ball:draw()
     --[[
-         I know these aren't circles, but as there's no current circle
-         implementation in Rockbox, rectangles will just do fine (drawing
-         circles from within Lua is far too slow).
-    ]]--
-    set_foreground(self.color)
-    rb.lcd_fillrect(self.x, self.y, self.size, self.size)
+    --set_foreground(self.color)
+    --rb.lcd_fillrect(self.x, self.y, self.size, self.size)
+    ]]
+
+    _ellipse(_LCD, self.x, self.y,
+             self.x + self.size, self.y + self.size , self.color, self.color, true)
+end
+
+function Ball:step_exploded()
+    if self.implosion and self.size > 0 then
+        self.size = self.size - 2
+        self.x = self.x + 1 -- We do this because we want to stay centered
+        self.y = self.y + 1
+    elseif self.size < self.explosion_size then
+        self.size = self.size + 2
+        self.x = self.x - 1 -- We do this for the same reasons as above
+        self.y = self.y - 1
+    end
 end
 
 function Ball:step()
-    if self.exploded then
-        if self.implosion and self.size > 0 then
-            self.size = self.size - 2
-            self.x = self.x + 1 -- We do this because we want to stay centered
-            self.y = self.y + 1
-        elseif self.size < self.explosion_size then
-            self.size = self.size + 2
-            self.x = self.x - 1 -- We do this for the same reasons as above
-            self.y = self.y - 1
-        end
-        return
-    end
-
     self.x = self.x + self.right_speed
     self.y = self.y + self.up_speed
-    if (self.right_speed > 0 and self.x + self.size >= rb.LCD_WIDTH) or
-       (self.right_speed < 0 and self.x <= 0) then
+    if (self.x <= 0 or self.x >= rb.LCD_WIDTH - self.size) then
         self.right_speed = -self.right_speed
+        self.x = self.x + self.right_speed
     end
-    if (self.up_speed > 0 and self.y + self.size >= rb.LCD_HEIGHT) or
-       (self.up_speed < 0 and self.y <= 0) then
+    if (self.y <= 0 or self.y >= rb.LCD_HEIGHT - self.size ) then
         self.up_speed = -self.up_speed
+        self.y = self.y + self.up_speed
     end
 end
 
@@ -137,7 +203,8 @@
 local Cursor = {
                 size = DEFAULT_BALL_SIZE*2,
                 x = rb.LCD_WIDTH/2,
-                y = rb.LCD_HEIGHT/2
+                y = rb.LCD_HEIGHT/2,
+                image = create_cursor(DEFAULT_BALL_SIZE*2)
              }
 
 function Cursor:new()
@@ -145,18 +212,18 @@
 end
 
 function Cursor:do_action(action)
-    if action == rb.actions.ACTION_TOUCHSCREEN and HAS_TOUCHSCREEN then
+    if action == actions_pla.ACTION_TOUCHSCREEN and HAS_TOUCHSCREEN then
         _, self.x, self.y = rb.action_get_touchscreen_press()
         return true
-    elseif action == rb.actions.PLA_SELECT then
+    elseif action == actions_pla.PLA_SELECT then
         return true
-    elseif (action == rb.actions.PLA_RIGHT) then
+    elseif (action == actions_pla.PLA_RIGHT or action == actions_pla.PLA_RIGHT_REPEAT) then
         self.x = self.x + self.size
-    elseif (action == rb.actions.PLA_LEFT) then
+    elseif (action == actions_pla.PLA_LEFT or action == actions_pla.PLA_LEFT_REPEAT) then
         self.x = self.x - self.size
-    elseif (action == rb.actions.PLA_UP) then
+    elseif (action == actions_pla.PLA_UP or action == actions_pla.PLA_UP_REPEAT) then
         self.y = self.y - self.size
-    elseif (action == rb.actions.PLA_DOWN) then
+    elseif (action == actions_pla.PLA_DOWN or action == actions_pla.PLA_DOWN_REPEAT) then
         self.y = self.y + self.size
     end
 
@@ -176,25 +243,16 @@
 end
 
 function Cursor:draw()
-    set_foreground(DEFAULT_FOREGROUND_COLOR)
 
-    rb.lcd_hline(self.x - self.size/2, self.x - self.size/4, self.y - self.size/2)
-    rb.lcd_hline(self.x + self.size/4, self.x + self.size/2, self.y - self.size/2)
-    rb.lcd_hline(self.x - self.size/2, self.x - self.size/4, self.y + self.size/2)
-    rb.lcd_hline(self.x + self.size/4, self.x + self.size/2, self.y + self.size/2)
-    rb.lcd_vline(self.x - self.size/2, self.y - self.size/2, self.y - self.size/4)
-    rb.lcd_vline(self.x - self.size/2, self.y + self.size/4, self.y + self.size/2)
-    rb.lcd_vline(self.x + self.size/2, self.y - self.size/2, self.y - self.size/4)
-    rb.lcd_vline(self.x + self.size/2, self.y + self.size/4, self.y + self.size/2)
-
-    rb.lcd_hline(self.x - self.size/4, self.x + self.size/4, self.y)
-    rb.lcd_vline(self.x, self.y - self.size/4, self.y + self.size/4)
+    rocklib_image.copy(_LCD, self.image, self.x - self.size/2, self.y - self.size/2,
+                       _NIL, _NIL, _NIL, _NIL, true, BSAND, DEFAULT_FOREGROUND_COLOR)
 end
 
 function draw_positioned_string(bottom, right, str)
     local _, w, h = rb.font_getstringsize(str, rb.FONT_UI)
-
-    rb.lcd_putsxy((rb.LCD_WIDTH-w)*right, (rb.LCD_HEIGHT-h)*bottom, str)
+    local x = not right or (rb.LCD_WIDTH-w)*right - 1
+    local y = not bottom or (rb.LCD_HEIGHT-h)*bottom - 1
+    rb.lcd_putsxy(x, y, str)
 end
 
 function set_foreground(color)
@@ -213,31 +271,43 @@
 
 function start_round(level, goal, nrBalls, total)
     local player_added, score, exit, nrExpendedBalls = false, 0, false, 0
-    local balls, explodedBalls = {}, {}
+    local Balls, explodedBalls = {}, {}
+    local ball_ct, ball_el
+    local tick, endtick
+    local action = 0
+    local hit_detected = false
     local cursor = Cursor:new()
 
     -- Initialize the balls
     for _=1,nrBalls do
-        table.insert(balls, Ball:new())
+        table.insert(Balls, Ball:new(nil, level))
+    end
+
+    local function draw_stats()
+        draw_positioned_string(0, 0, string.format("%d balls expended", nrExpendedBalls))
+        draw_positioned_string(0, 1, string.format("Level %d", level))
+        draw_positioned_string(1, 1, string.format("%d level points", score))
+        draw_positioned_string(1, 0, string.format("%d total points", total + score))
     end
 
     -- Make sure there are no unwanted touchscreen presses
     rb.button_clear_queue()
 
+    set_foreground(DEFAULT_FOREGROUND_COLOR) -- color for text
+
     while true do
-        local endtick = rb.current_tick() + CYCLETIME
+        endtick = rb.current_tick() + CYCLETIME
 
         -- Check if the round is over
-        if #explodedBalls == 0 and player_added then
+        if player_added and #explodedBalls == 0 then
             break
         end
 
-        -- Check for actions
-        local action = rb.get_plugin_action(0)
-        if action == rb.actions.PLA_EXIT or action == rb.actions.PLA_CANCEL then
+        if action == actions_pla.PLA_EXIT or action == actions_pla.PLA_CANCEL then
             exit = true
             break
         end
+
         if not player_added and cursor:do_action(action) then
             local player = Ball:new({
                                 x = cursor.x,
@@ -248,26 +318,28 @@
                                 exploded = true,
                                 death_time = rb.current_tick() + rb.HZ * 3
                             })
-            table.insert(explodedBalls, player)
+            explodedBalls[1] = player
             player_added = true
+            cursor = nil
         end
 
-        -- Check for hits
-        for i, ball in ipairs(balls) do
+       -- Check for hits
+        for i, Ball in ipairs(Balls) do
             for _, explodedBall in ipairs(explodedBalls) do
-                if ball:checkHit(explodedBall) then
-                    score = score + 100*level
-                    nrExpendedBalls = nrExpendedBalls + 1
-                    table.insert(explodedBalls, ball)
-                    table.remove(balls, i)
+                if Ball:checkHit(explodedBall) then
+                    explodedBalls[#explodedBalls + 1] = Ball
+                    --table.remove(Balls, i)
+                    Balls[i] = false
+                    hit_detected = true
                     break
                 end
             end
         end
 
         -- Check if we're dead yet
+        tick = rb.current_tick()
         for i, explodedBall in ipairs(explodedBalls) do
-            if rb.current_tick() >= explodedBall.death_time then
+            if explodedBall.death_time < tick then
                 if explodedBall.size > 0 then
                     explodedBall.implosion = true -- We should be dying
                 else
@@ -277,38 +349,64 @@
         end
 
         -- Drawing phase
+        if hit_detected then
+            hit_detected = false
+            -- same as table.remove(Balls, i) but more efficient
+            ball_el = 1
+            ball_ct = #Balls
+            for i = 1, ball_ct do
+                if Balls[i] then
+                    Balls[ball_el] = Balls[i]
+                    ball_el = ball_el + 1
+                end
+            end
+            -- remove any remaining exploded balls
+            for i = ball_el, ball_ct do
+                Balls[i] = nil
+            end
+            -- Calculate score
+            nrExpendedBalls = nrBalls - ball_el + 1
+            score = nrExpendedBalls * level * 100
+        end
+
         rb.lcd_clear_display()
+        draw_stats()
 
-        set_foreground(DEFAULT_FOREGROUND_COLOR)
-        draw_positioned_string(0, 0, string.format("%d balls expended", nrExpendedBalls))
-        draw_positioned_string(0, 1, string.format("Level %d", level))
-        draw_positioned_string(1, 1, string.format("%d level points", score))
-        draw_positioned_string(1, 0, string.format("%d total points", total+score))
+        if not (player_added or HAS_TOUCHSCREEN) then
+            cursor:draw()
+        end
 
-        for _, ball in ipairs(balls) do
-            ball:step()
-            ball:draw()
+        for _, Ball in ipairs(Balls) do
+            Ball:step()
+            Ball:draw()
         end
 
         for _, explodedBall in ipairs(explodedBalls) do
-            explodedBall:step()
-            explodedBall:draw()
-        end
-
-        if not HAS_TOUCHSCREEN and not player_added then
-            cursor:draw()
+            explodedBall:step_exploded()
+            explodedBall:draw_exploded()
         end
 
         -- Push framebuffer to the LCD
         rb.lcd_update()
 
+        -- Check for actions
         if rb.current_tick() < endtick then
-            rb.sleep(endtick - rb.current_tick())
+            action = rb.get_plugin_action(endtick - rb.current_tick())
         else
             rb.yield()
+            action = rb.get_plugin_action(0)
         end
+
     end
 
+    --splash the final stats for a moment at end
+    rb.lcd_clear_display()
+    for _, Ball in ipairs(Balls) do Ball:draw() end
+    _LCD:clear(nil, nil, nil, nil, nil, nil, 2, 2)
+    draw_stats()
+    rb.lcd_update()
+    rb.sleep(rb.HZ * 2)
+
     return exit, score, nrExpendedBalls
 end
 
@@ -320,6 +418,7 @@
 
     rb.lcd_clear_display()
     set_foreground(DEFAULT_FOREGROUND_COLOR)
+
     if w > rb.LCD_WIDTH then
         rb.lcd_puts_scroll(0, y/h, message)
     else
@@ -340,7 +439,7 @@
     if to == -1 then
         rb.sleep(rb.HZ/2)
         rb.button_clear_queue()
-        rb.button_get(1)
+        rb.button_get(rb.HZ * 60)
     else
         rb.sleep(to)
     end
@@ -351,6 +450,8 @@
 if HAS_TOUCHSCREEN then
     rb.touchscreen_set_mode(rb.TOUCHSCREEN_POINT)
 end
+
+--[[MAIN PROGRAM]]
 rb.backlight_force_on()
 
 math.randomseed(os.time())
@@ -359,9 +460,12 @@
 while levels[idx] ~= nil do
     local goal, nrBalls = levels[idx][1], levels[idx][2]
 
+    collectgarbage("collect") --run gc now to hopefully prevent interruption later
+
     display_message(rb.HZ*2, "Level %d: get %d out of %d balls", idx, goal, nrBalls)
 
     local exit, score, nrExpendedBalls = start_round(idx, goal, nrBalls, highscore)
+
     if exit then
         break -- Exiting..
     else
@@ -371,11 +475,14 @@
             highscore = highscore + score
         else
             display_message(rb.HZ*2, "You lost!")
+            highscore = highscore - score - idx * 100
+            if highscore < 0 then break end
         end
     end
 end
-
-if idx > #levels then
+if highscore <= 0 then
+    display_message(-1, "You lost at level %d", idx)
+elseif idx > #levels then
     display_message(-1, "You finished the game with %d points!", highscore)
 else
     display_message(-1, "You made it till level %d with %d points!", idx, highscore)
@@ -383,3 +490,4 @@
 
 -- Restore user backlight settings
 rb.backlight_use_settings()
+