local tau = math.pi * 2 local game = {} local function load_level(level, bubble_diameter, row_gap) local slots = {} local bubble_radius = bubble_diameter / 2 local num_rows = #level local max_cols = math.max(#level[1], #level[2]) local min_cols = math.min(#level[1], #level[2]) assert( max_cols == min_cols + 1, 'number of columns in alternating rows must differ by one' ) local num_bubble_slots = 0 if num_rows % 2 == 0 then num_bubble_slots = num_rows / 2 * #level[1] + num_rows / 2 * #level[2] else num_bubble_slots = math.ceil(num_rows / 2) * #level[1] + math.floor(num_rows / 2) * #level[2] end print('slots', num_bubble_slots) for row = 1, #level do local num_cols = #level[row] assert( num_cols == min_cols or num_cols == max_cols, string.format("wrong number of columns (%d) in row %d", num_cols, row) ) local x_offset = num_cols == min_cols and bubble_radius or 0 for col = 1, num_cols do local index = #slots + 1 slots[index] = { x = x_offset + bubble_radius + (col - 1) * bubble_diameter, y = bubble_radius + (row - 1) * row_gap, bubble_type = level[row][col], neighbours = {} } if col > 1 then -- left slots[index].neighbours[#slots[index].neighbours+1] = index - 1 end if col < num_cols then -- right slots[index].neighbours[#slots[index].neighbours+1] = index + 1 end if row % 2 == 0 or (row > 1 and col > 1) then -- up left slots[index].neighbours[#slots[index].neighbours+1] = index - max_cols end if row % 2 == 0 or (row > 1 and col < num_cols) then -- up right slots[index].neighbours[#slots[index].neighbours+1] = index - min_cols end if row % 2 == 0 or col > 1 then -- down left if index + min_cols <= num_bubble_slots then slots[index].neighbours[#slots[index].neighbours+1] = index + min_cols end end if row % 2 == 0 or col < num_cols then -- down right if index + max_cols <= num_bubble_slots then slots[index].neighbours[#slots[index].neighbours+1] = index + max_cols end end end end return slots end -- ex. remaining_bubble_types = {1,4,5,8} -- should only be regular, not special local function get_next_bubble_type(index, remaining_bubble_types) return remaining_bubble_types[love.math.random(#remaining_bubble_types)] end local function get_bubble_counts() local total = 0 local count_by_type = {} for i = 1, #game.bubble_slots do local bubble_type = game.bubble_slots[i].bubble_type if bubble_type ~= 0 then total = total + 1 end if count_by_type[bubble_type] == nil then count_by_type[bubble_type] = 0 end count_by_type[bubble_type] = count_by_type[bubble_type] + 1 end return total, count_by_type end local function find_nearest_slot(x, y) local nearest_slot_index = 0 local min_dist = math.huge for i = 1, #game.bubble_slots do local slot = game.bubble_slots[i] if slot.bubble_type == 0 then local diff_x = x - slot.x local diff_y = y - slot.y local dist = math.sqrt(diff_x * diff_x + diff_y * diff_y) if dist < min_dist then min_dist = dist nearest_slot_index = i end end end assert(nearest_slot_index ~= 0, "no nearest slot found") return nearest_slot_index end local function find_matches(start_index) local bubble_type = game.bubble_slots[start_index].bubble_type local to_check = {start_index} local matches = {start_index} local visited = {} visited[start_index] = true while #to_check > 0 do local current_index = table.remove(to_check) local slot = game.bubble_slots[current_index] for i = 1, #slot.neighbours do local neighbour_index = slot.neighbours[i] local neighbour = game.bubble_slots[neighbour_index] if not visited[neighbour_index] and neighbour.bubble_type == bubble_type then matches[#matches+1] = neighbour_index to_check[#to_check+1] = neighbour_index end visited[neighbour_index] = true end end return matches end function love.load(arg) if arg[#arg] == "debug" then require("lldebugger").start() end game.window_width, game.window_height = love.graphics.getDimensions() game.paused = false game.frame_by_frame = false game.bubble_diameter = 60 game.bubble_radius = game.bubble_diameter / 2 game.bubble_speed = 8 -- px/frame -- vertical distance between bubble center points game.row_gap = math.ceil(math.sqrt( (game.bubble_diameter * game.bubble_diameter) - (game.bubble_radius * game.bubble_radius) )) game.background_image = love.graphics.newImage('images/background.png') game.background_width = game.background_image:getWidth() game.bubble_images = { love.graphics.newImage('images/red.png'), -- 1 love.graphics.newImage('images/orange.png'), -- 2 love.graphics.newImage('images/yellow.png'), -- 3 love.graphics.newImage('images/green.png'), -- 4 love.graphics.newImage('images/blue.png'), -- 5 love.graphics.newImage('images/purple.png'), -- 6 love.graphics.newImage('images/pink.png'), -- 7 love.graphics.newImage('images/white.png') -- 8 } game.levels = {} game.levels[1] = { {1,1,1,3,3,2,2,2}, { 1,1,3,3,3,2,2 }, {4,4,7,8,8,6,5,5}, { 4,7,7,8,6,6,5 }, {0,0,0,0,0,0,0,0}, { 0,0,0,0,0,0,0 }, {0,0,0,0,0,0,0,0}, { 0,0,0,0,0,0,0 }, {0,0,0,0,0,0,0,0}, { 0,0,0,0,0,0,0 }, {0,0,0,0,0,0,0,0}, { 0,0,0,0,0,0,0 }, {0,0,0,0,0,0,0,0}, { 0,0,0,0,0,0,0 } } game.current_level = 1 game.bubble_slots = load_level( game.levels[game.current_level], game.bubble_diameter, game.row_gap ) local max_cols = math.max( #game.levels[game.current_level][1], #game.levels[game.current_level][2] ) local num_rows = #game.levels[game.current_level] game.level_width = max_cols * game.bubble_diameter game.level_height = (num_rows - 1) * game.row_gap + game.bubble_diameter game.level_left = (game.window_width - game.level_width) / 2 game.level_top = game.bubble_radius game.level_right = game.level_left + game.level_width game.level_bottom = game.level_top + game.level_height for i = 1, #game.bubble_slots do game.bubble_slots[i].x = game.bubble_slots[i].x + game.level_left game.bubble_slots[i].y = game.bubble_slots[i].y + game.level_top end game.launcher_image = love.graphics.newImage('images/launcher.png') game.launcher_height = game.launcher_image:getHeight() game.launcher_width = game.launcher_image:getWidth() game.launcher_x = game.window_width / 2 game.launcher_y = game.level_top + game.level_height + game.bubble_diameter game.launcher_offset_x = 90 -- rotation point in launcher image game.launcher_offset_y = game.launcher_height / 2 game.launcher_rotation = -tau / 4 -- up local total_bubble_count, counts_by_type = get_bubble_counts() local bubble_types = {} for bubble_type, count in pairs(counts_by_type) do if bubble_type >= 1 and bubble_type <= 8 then bubble_types[#bubble_types+1] = bubble_type end end game.next_bubble_index = 1 game.next_bubble = { x = game.launcher_x, y = game.launcher_y, bubble_type = get_next_bubble_type(game.next_bubble_index, bubble_types), velocity_x = 0, velocity_y = 0 } end function love.update(dt) if game.paused then return end if game.next_bubble.velocity_x ~= 0 or game.next_bubble.velocity_y ~= 0 then local next_x = game.next_bubble.x + game.next_bubble.velocity_x local next_y = game.next_bubble.y + game.next_bubble.velocity_y local bubble_should_stop = false -- collision with ceiling if next_y - game.bubble_radius <= game.level_top then bubble_should_stop = true end -- collision with walls if not bubble_should_stop then if next_x + game.bubble_radius >= game.level_right or next_x - game.bubble_radius <= game.level_left then local impact_x = game.level_left + game.bubble_radius if next_x + game.bubble_radius >= game.level_right then impact_x = game.level_right - game.bubble_radius end local dx = impact_x - game.next_bubble.x local ratio = dx / game.next_bubble.velocity_x local dy = game.next_bubble.velocity_y * ratio local impact_y = game.next_bubble.y + dy local ratio_after_bounce = 1.0 - ratio game.next_bubble.velocity_x = -game.next_bubble.velocity_x next_x = impact_x + game.next_bubble.velocity_x * ratio_after_bounce next_y = impact_y + game.next_bubble.velocity_y * ratio_after_bounce end end -- collision with another bubble if not bubble_should_stop then for i = 1, #game.bubble_slots do local slot = game.bubble_slots[i] if slot.bubble_type ~= 0 then local diff_x = next_x - slot.x local diff_y = next_y - slot.y local dist = math.sqrt(diff_x * diff_x + diff_y * diff_y) if dist <= game.bubble_diameter - 2 then -- -2 to allow squeezing through local depth = game.bubble_diameter - dist local direction_x = diff_x / dist local direction_y = diff_y / dist next_x = next_x + direction_x * depth next_y = next_y + direction_y * depth bubble_should_stop = true break end end end end if bubble_should_stop then local nearest_slot_index = find_nearest_slot(next_x, next_y) game.bubble_slots[nearest_slot_index].bubble_type = game.next_bubble.bubble_type game.next_bubble.x = game.bubble_slots[nearest_slot_index].x game.next_bubble.y = game.bubble_slots[nearest_slot_index].y game.next_bubble.velocity_x = 0 game.next_bubble.velocity_y = 0 local matches = find_matches(nearest_slot_index) else game.next_bubble.x = next_x game.next_bubble.y = next_y end end if game.frame_by_frame then game.paused = true game.frame_by_frame = false end end function love.draw(alpha) if game.paused then alpha = 0 end -- draw background love.graphics.setColor(1, 1, 1, 1) love.graphics.draw(game.background_image, 0, 0) love.graphics.draw(game.background_image, game.background_width, 0) -- draw stationary bubbles love.graphics.setColor(1, 1, 1, 1) for i = 1, #game.bubble_slots do local slot = game.bubble_slots[i] if slot.bubble_type >= 1 and slot.bubble_type <= #game.bubble_images then love.graphics.draw( game.bubble_images[slot.bubble_type], slot.x, slot.y, 0, 1, 1, game.bubble_radius, game.bubble_radius ) else love.graphics.circle( 'line', slot.x, slot.y, game.bubble_radius ) end end -- draw walls love.graphics.setColor(0, 0, 0, 1) love.graphics.rectangle( 'line', game.level_left, game.level_top, game.level_width, game.level_height ) -- draw launcher love.graphics.setColor(1, 1, 1, 1) love.graphics.draw( game.launcher_image, game.launcher_x, game.launcher_y, game.launcher_rotation, 1, 1, game.launcher_offset_x, game.launcher_offset_y ) -- draw next bubble -- if moving, extrapolate position based on alpha value from love.run love.graphics.setColor(1, 1, 1, 1) love.graphics.draw( game.bubble_images[game.next_bubble.bubble_type], game.next_bubble.x + game.next_bubble.velocity_x * alpha, game.next_bubble.y + game.next_bubble.velocity_y * alpha, 0, 1, 1, game.bubble_radius, game.bubble_radius ) end function love.mousepressed(x, y, button, is_touch, presses) if button == 1 then local diff_x = x - game.launcher_x local diff_y = y - game.launcher_y local dist = math.sqrt(diff_x * diff_x + diff_y * diff_y) game.next_bubble.velocity_x = diff_x / dist * game.bubble_speed game.next_bubble.velocity_y = diff_y / dist * game.bubble_speed end end function love.mousemoved(x, y, dx, dy, is_touch) local diff_x = x - game.launcher_x local diff_y = y - game.launcher_y game.launcher_rotation = math.atan2(diff_y, diff_x) end function love.keypressed(key, scan_code, is_repeat) if key == 'escape' then love.event.quit() elseif key == 'r' then game.next_bubble.x = game.launcher_x game.next_bubble.y = game.launcher_y game.next_bubble.velocity_x = 0 game.next_bubble.velocity_y = 0 game.next_bubble.bubble_type = get_next_bubble_type( game.next_bubble_index, {1,2,3,4,5,6,7,8} ) game.paused = false elseif key == 'space' then game.paused = not game.paused elseif key == 'n' and game.paused then game.frame_by_frame = true game.paused = false end end function love.run() if love.load then love.load(love.arg.parseGameArguments(arg), arg) end if love.timer then love.timer.step() end local dt = 1/120 -- update 120 times per second local accumulator = 0.0 return function() if love.event then love.event.pump() for name, a,b,c,d,e,f in love.event.poll() do if name == "quit" then if not love.quit or not love.quit() then return a or 0 end end love.handlers[name](a,b,c,d,e,f) end end local frame_time = dt -- default in case love.timer is disabled if love.timer then frame_time = love.timer.step() if frame_time > 0.25 then frame_time = 0.25 end end accumulator = accumulator + frame_time while (accumulator >= dt) do if love.update then love.update(dt) end accumulator = accumulator - dt end local alpha = accumulator / dt if love.graphics and love.graphics.isActive() then love.graphics.origin() love.graphics.clear(love.graphics.getBackgroundColor()) if love.draw then love.draw(alpha) end love.graphics.present() end if love.timer then love.timer.sleep(0.001) end end end