0,]']>S}Wup8d%qy)6o;]AsVl3= PLAYER_ANIM.delay then PLAYER_ANIM.timer = 0 PLAYER_ANIM.frame = PLAYER_ANIM.frame % #PLAYER_ANIM.frames + 1 end if GAME.state == "menu" then update_menu() elseif GAME.state == "playing" then if CONFIG.DEBUG and LAST_UNSOLVABLE_SEED ~= nil and keyp(4) then -- 4 is keyboard 'D' GAME.seed = LAST_UNSOLVABLE_SEED init_game() return end if btnp(CONFIG.BUTTONS.B) or keyp(CONFIG.BUTTONS.BACKSPACE) then GAME.state = "menu" return end update_player() update_camera() elseif GAME.state == "win" then if btnp(CONFIG.BUTTONS.B) or keyp(CONFIG.BUTTONS.BACKSPACE) then LAST_UNSOLVABLE_SEED = nil -- Clear the bad seed when returning to menu GAME.state = "menu" return end if btnp(CONFIG.BUTTONS.A) or keyp(CONFIG.BUTTONS.SPACE) or keyp(CONFIG.BUTTONS.ENTER) then -- Start new game with same parameters but new seed GAME.seed = math.random(99999) init_game() return end end end function draw_menu() rect(0, 0, CONFIG.SCREEN_W, CONFIG.SCREEN_H, 0) print("DOOR RUNNER", 88, 20, 14) local menu_items = {"MAZE SIZE", "KEYS/DOORS", "START NEW"} local settings = GAME.menu.settings for i, item in ipairs(menu_items) do local y = 50 + (i-1) * 20 local color = (i == GAME.menu.selection) and 12 or 14 if i == GAME.menu.selection then print(">", 60, y, 13) end print(item, 70, y, color) if i == 1 then local size_name = settings.size_options[settings.size_index].n print("< "..size_name.." >", 150, y, color) elseif i == 2 then print("< "..settings.key_count.." >", 150, y, color) end end print("[A]/[ENTER] START", 40, 110, 15) print("[B] RESUME", 140, 110, 15) if CONFIG.DEBUG and LAST_UNSOLVABLE_SEED then print("[D] BAD SEED: "..LAST_UNSOLVABLE_SEED, 8, 0, 2) -- red end end function update_menu() local navigated = false if btnp(CONFIG.BUTTONS.DOWN) then GAME.menu.selection = GAME.menu.selection % 3 + 1 navigated = true elseif btnp(CONFIG.BUTTONS.UP) then GAME.menu.selection = (GAME.menu.selection - 2 + 3) % 3 + 1 navigated = true end local settings = GAME.menu.settings if GAME.menu.selection == 1 then if btnp(CONFIG.BUTTONS.RIGHT) or btnp(CONFIG.BUTTONS.LEFT) then settings.size_index = (settings.size_index + (#settings.size_options + (btnp(CONFIG.BUTTONS.RIGHT) and 1 or -1)) -1) % #settings.size_options + 1 navigated = true end elseif GAME.menu.selection == 2 then if btnp(CONFIG.BUTTONS.RIGHT) then settings.key_count = math.min(#CONFIG.COLOR_DATA, settings.key_count + 1) navigated = true elseif btnp(CONFIG.BUTTONS.LEFT) then settings.key_count = math.max(0, settings.key_count - 1) navigated = true end end if navigated then sfx(CONFIG.SFX.MENU_NAV) end if btnp(CONFIG.BUTTONS.B) or keyp(CONFIG.BUTTONS.BACKSPACE) then if GAME.seed ~= nil then sfx(CONFIG.SFX.MENU_SELECT) GAME.state = "playing" end end if btnp(CONFIG.BUTTONS.A) or keyp(CONFIG.BUTTONS.ENTER) or keyp(CONFIG.BUTTONS.SPACE) then sfx(CONFIG.SFX.MENU_SELECT) start_game_from_menu() end end function update_player() local tx, ty = GAME.player.x, GAME.player.y local speed = CONFIG.TILE_SIZE local hold, period = 15, 4 if btnp(CONFIG.BUTTONS.UP, hold, period) then ty = ty - speed end if btnp(CONFIG.BUTTONS.DOWN, hold, period) then ty = ty + speed end if btnp(CONFIG.BUTTONS.LEFT, hold, period) then tx = tx - speed end if btnp(CONFIG.BUTTONS.RIGHT, hold, period) then tx = tx + speed end if tx ~= GAME.player.x or ty ~= GAME.player.y then local mx, my = tx / CONFIG.TILE_SIZE, ty / CONFIG.TILE_SIZE if mx >= 0 and mx < CONFIG.MAP_W and my >= 0 and my < CONFIG.MAP_H then local tile_id = mget(mx, my) if can_move_to(tile_id) then sfx(CONFIG.SFX.MOVE) GAME.player.x, GAME.player.y = tx, ty GAME.stats.moves = GAME.stats.moves + 1 handle_item_interaction(tile_id, mx, my) end end end end function update_camera() local deadzone_w, deadzone_h = 100, 60 local dz_left = (CONFIG.SCREEN_W - deadzone_w) / 2 local dz_right = dz_left + deadzone_w local dz_top = (CONFIG.SCREEN_H - deadzone_h) / 2 local dz_bottom = dz_top + deadzone_h local p_screen_x = GAME.player.x - GAME.camera.x local p_screen_y = GAME.player.y - GAME.camera.y local target_cam_x, target_cam_y = GAME.camera.x, GAME.camera.y local p_width = CONFIG.TILE_SIZE if p_screen_x < dz_left then target_cam_x = GAME.player.x - dz_left elseif p_screen_x > dz_right - p_width then target_cam_x = GAME.player.x - (dz_right - p_width) end if p_screen_y < dz_top then target_cam_y = GAME.player.y - dz_top elseif p_screen_y > dz_bottom - p_width then target_cam_y = GAME.player.y - (dz_bottom - p_width) end local max_cam_x = math.max(0, CONFIG.MAP_W * CONFIG.TILE_SIZE - CONFIG.SCREEN_W) local max_cam_y = math.max(0, CONFIG.MAP_H * CONFIG.TILE_SIZE - CONFIG.SCREEN_H) target_cam_x = math.max(0, math.min(target_cam_x, max_cam_x)) target_cam_y = math.max(0, math.min(target_cam_y, max_cam_y)) local smoothness = 0.1 GAME.camera.x = GAME.camera.x + (target_cam_x - GAME.camera.x) * smoothness GAME.camera.y = GAME.camera.y + (target_cam_y - GAME.camera.y) * smoothness end -------------------------------------------------------------------------------- -- :: DRAWING :: -------------------------------------------------------------------------------- function TIC() update() cls(0) if GAME.state == "menu" then if GAME.seed ~= nil then draw_world() end draw_menu() else draw_world() draw_ui() draw_overlays() end end function draw_world() local cam_x = math.floor(GAME.camera.x) local cam_y = math.floor(GAME.camera.y) map(0, 0, CONFIG.MAP_W, CONFIG.MAP_H, -cam_x, -cam_y) -- Draw animated player sprite spr(PLAYER_ANIM.frames[PLAYER_ANIM.frame], GAME.player.x - cam_x, GAME.player.y - cam_y,0) end function draw_ui() if CONFIG.DEBUG and LAST_UNSOLVABLE_SEED then print("BAD SEED: "..LAST_UNSOLVABLE_SEED.." (D)", 0, 0, 2) -- red end rect(0, CONFIG.SCREEN_H - CONFIG.UI_H, CONFIG.SCREEN_W, CONFIG.UI_H, 0) local ui_y = CONFIG.SCREEN_H - 8 print("KEYS:", 0, ui_y, 15) for i, color_data in ipairs(CONFIG.COLOR_DATA) do local x = 20 + i * 7 local key_sprite = GAME.player.keys[color_data.name] and color_data.key or CONFIG.SPRITES.GRAY_KEY spr(key_sprite, x, ui_y) end -- Draw eggs collected print("EGGS:", 80, ui_y, 15) for i = 1, (GAME.player.total_eggs or 3) do local x = 100 + i * 9 local egg_collected = (GAME.player.eggs or 0) >= i spr(egg_collected and CONFIG.SPRITES.EGG or CONFIG.SPRITES.GREY_EGG, x, ui_y) end if CONFIG.DEBUG then local attempt_str = GAME.stats.generation_attempts.."/"..20 print("SEED:" .. GAME.seed.." ("..attempt_str..")", 140, ui_y, 15) else print("SEED:" .. GAME.seed, 170, ui_y, 15) end end function draw_overlays() if GAME.state == "win" then local time_str = format_time(GAME.stats.end_time - GAME.stats.start_time) -- Win box dimensions local x, y, w, h = 60, 32, 120, 68 -- 2px green border (color 11) rect(x-2, y-2, w+4, h+4, 11) rect(x-1, y-1, w+2, h+2, 11) -- Black background rect(x, y, w, h, 0) -- Golden header (color 10) rect(x, y, w, 16, 10) -- "YOU WIN!" centered, bright white (color 15) print("YOU WIN!", x +10 , y + 4, 15, true, 2) -- Stats: brighter color (color 12) print("TIME: "..time_str, x + 12, y + 22, 13) print("MOVES: "..GAME.stats.moves, x + 12, y + 32, 13) print("SCORE: "..GAME.stats.score, x + 12, y + 42, 12) -- Info text: dark gray (color 13) --print("[B] MENU", x + 12, y + 52, 14) print("[A] NEW GAME", x + 12, y + 58, 14) end end -------------------------------------------------------------------------------- -- :: GAME LOGIC & RULES :: -------------------------------------------------------------------------------- function remove_doors_of_color(door_id) for y = 0, CONFIG.MAP_H - 1 do for x = 0, CONFIG.MAP_W - 1 do if mget(x, y) == door_id then mset(x, y, CONFIG.SPRITES.GRAY_DOOR) end end end end function can_move_to(id) if id == CONFIG.SPRITES.WALL then return false end for _, color_data in ipairs(CONFIG.COLOR_DATA) do if id == color_data.door and not GAME.player.keys[color_data.name] then return false end end return true end function handle_item_interaction(id, mx, my) local door_to_remove = nil for _, color_data in ipairs(CONFIG.COLOR_DATA) do if id == color_data.key then sfx(CONFIG.SFX.KEY_PICKUP) GAME.player.keys[color_data.name] = true door_to_remove = color_data.door break end end if id == CONFIG.SPRITES.EGG then GAME.player.eggs = (GAME.player.eggs or 0) + 1 mset(mx, my, CONFIG.SPRITES.EMPTY) sfx(CONFIG.SFX.KEY_PICKUP) end if id == CONFIG.SPRITES.GOAL then sfx(CONFIG.SFX.WIN) GAME.stats.end_time = time() calculate_final_score() GAME.state = "win" end if door_to_remove then mset(mx, my, CONFIG.SPRITES.EMPTY) remove_doors_of_color(door_to_remove) end end function calculate_final_score() local stats = GAME.stats local settings = GAME.menu.settings local time_taken_cs = stats.end_time - stats.start_time local time_taken_s = time_taken_cs / 100 local size_bonus = settings.size_index * 10000 local key_bonus = settings.key_count * 5000 local egg_bonus = (GAME.player.eggs or 0) * 2000 local max_score = 50000 + size_bonus + key_bonus + egg_bonus local time_penalty = time_taken_s * 100 local move_penalty = stats.moves * 50 local final_score = max_score - time_penalty - move_penalty stats.score = math.floor(math.max(0, final_score)) end -------------------------------------------------------------------------------- -- :: MAZE GENERATION & ITEM PLACEMENT :: -------------------------------------------------------------------------------- function generate_maze() for y=0, CONFIG.MAP_H-1 do for x=0, CONFIG.MAP_W-1 do mset(x,y,CONFIG.SPRITES.WALL) end end local stack, visited = {}, {} for y=0, CONFIG.MAP_H-1 do visited[y]={} end local cx, cy = 1, 1 visited[cy][cx] = true table.insert(stack, {x=cx, y=cy}) while #stack > 0 do cx,cy = stack[#stack].x, stack[#stack].y mset(cx, cy, CONFIG.SPRITES.EMPTY) local neighbors = {} for _,d in ipairs({{0,-2},{0,2},{-2,0},{2,0}}) do local nx,ny = cx+d[1], cy+d[2] if nx>0 and nx0 and ny 0 then local next = neighbors[math.random(#neighbors)] mset(next.wx, next.wy, CONFIG.SPRITES.EMPTY) visited[next.y][next.x] = true table.insert(stack, {x=next.x, y=next.y}) else table.remove(stack) end end end function place_game_items() local goal_pos = find_furthest_tile(1, 1) mset(goal_pos.x, goal_pos.y, CONFIG.SPRITES.GOAL) -- Always place eggs, even if there are no keys/doors local key_info, all_key_positions = {}, {} if GAME.menu.settings.key_count > 0 then local dists_from_start = find_distances_from({x=1, y=1}) local paths = find_and_measure_dead_ends(goal_pos, dists_from_start) table.sort(paths, function(a, b) return a.score > b.score end) local keys_to_place = {} for i=1, GAME.menu.settings.key_count do table.insert(keys_to_place, CONFIG.COLOR_DATA[i]) end shuffle_table(keys_to_place) for i=1, math.min(#keys_to_place, #paths) do local p, c = paths[i], keys_to_place[i] local key_pos = {x=p.pos.x, y=p.pos.y} mset(key_pos.x, key_pos.y, c.key) table.insert(key_info, {color_data = c, key_pos = key_pos}) table.insert(all_key_positions, key_pos) end if #key_info > 0 then for _, info in ipairs(key_info) do info.dist = dists_from_start[info.key_pos.y][info.key_pos.x] or 9999 end table.sort(key_info, function(a, b) return a.dist < b.dist end) local MIN_KEY_DOOR_DISTANCE = 3 local MIN_START_DOOR_DISTANCE = 4 local start_pos = {x=1, y=1} for i=1, #key_info - 1 do local door_color = key_info[i].color_data.door local prerequisite_key = key_info[i] local target_key = key_info[i+1] local path_to_target = trace_path_to_junction(target_key.key_pos.x, target_key.key_pos.y) local path_to_prereq = find_path(start_pos, prerequisite_key.key_pos) local door_pos = find_safe_spot_on_path(path_to_target, all_key_positions, target_key.key_pos, MIN_KEY_DOOR_DISTANCE, start_pos, MIN_START_DOOR_DISTANCE, path_to_prereq) mset(door_pos.x, door_pos.y, door_color) end local final_door_color = key_info[#key_info].color_data.door local final_key = key_info[#key_info] local path_to_goal = trace_path_to_junction(goal_pos.x, goal_pos.y) local path_to_final_key = find_path(start_pos, final_key.key_pos) local goal_door_pos = find_safe_spot_on_path(path_to_goal, all_key_positions, goal_pos, MIN_KEY_DOOR_DISTANCE, start_pos, MIN_START_DOOR_DISTANCE, path_to_final_key) mset(goal_door_pos.x, goal_door_pos.y, final_door_color) end end -- Place 3 eggs (sprite 6) randomly on empty tiles, not on keys, doors, goal, or player start local forbidden = {} forbidden["1,1"] = true forbidden[goal_pos.x..","..goal_pos.y] = true -- Add doors and keys to forbidden if any if key_info then for _, info in ipairs(key_info) do forbidden[info.key_pos.x..","..info.key_pos.y] = true end end for y=0,CONFIG.MAP_H-1 do for x=0,CONFIG.MAP_W-1 do local tid = mget(x, y) for _, color_data in ipairs(CONFIG.COLOR_DATA) do if tid == color_data.door or tid == color_data.key then forbidden[x..","..y] = true end end end end local empty_tiles = {} for y=0,CONFIG.MAP_H-1 do for x=0,CONFIG.MAP_W-1 do if mget(x, y) == CONFIG.SPRITES.EMPTY and not forbidden[x..","..y] then table.insert(empty_tiles, {x=x, y=y}) end end end shuffle_table(empty_tiles) for i=1,math.min(3,#empty_tiles) do local pos = empty_tiles[i] mset(pos.x, pos.y, CONFIG.SPRITES.EGG) end end -------------------------------------------------------------------------------- -- :: HELPER FUNCTIONS (Maze Analysis & Utility) :: -------------------------------------------------------------------------------- function format_time(centiseconds) local total_seconds = math.floor(centiseconds / 100) local minutes = math.floor(total_seconds / 60) local seconds = total_seconds % 60 return string.format("%02d:%02d", minutes, seconds) end function shuffle_table(t) for i = #t, 2, -1 do local j = math.random(i) t[i], t[j] = t[j], t[i] end return t end function manhattan_distance(p1, p2) return math.abs(p1.x - p2.x) + math.abs(p1.y - p2.y) end function trace_path_to_junction(sx, sy) local path, px, py, cx, cy = {}, -1, -1, sx, sy while count_neighbors(cx, cy) <= 2 do table.insert(path, {x=cx, y=cy}) local moved = false for _,d in ipairs({{0,-1},{0,1},{-1,0},{1,0}}) do local nx,ny = cx+d[1], cy+d[2] if mget(nx,ny)~=CONFIG.SPRITES.WALL and(nx~=px or ny~=py)then px, py = cx, cy; cx, cy = nx, ny; moved = true; break end end if not moved or #path > CONFIG.MAP_W*CONFIG.MAP_H then break end end table.insert(path, {x=cx, y=cy}) return path end function find_path(p1, p2) local dists = find_distances_from(p1) if not dists[p2.y] or not dists[p2.y][p2.x] then return nil end local path = {} local curr = {x=p2.x, y=p2.y} while curr.x ~= p1.x or curr.y ~= p1.y do table.insert(path, 1, {x=curr.x, y=curr.y}) local current_dist = dists[curr.y][curr.x] local found_prev = false for _,d in ipairs({{0,-1},{0,1},{-1,0},{1,0}}) do local prev = {x=curr.x - d[1], y=curr.y - d[2]} if dists[prev.y] and dists[prev.y][prev.x] and dists[prev.y][prev.x] == current_dist - 1 then curr = prev found_prev = true break end end if not found_prev then return nil end end table.insert(path, 1, {x=p1.x, y=p1.y}) return path end function find_safe_spot_on_path(path_to_target, all_key_positions, self_pos, min_dist, start_pos, min_dist_from_start, prerequisite_path) for i = #path_to_target, 1, -1 do local candidate = path_to_target[i] local is_safe = true if manhattan_distance(candidate, start_pos) < min_dist_from_start then is_safe = false end if is_safe and prerequisite_path then for _, p_node in ipairs(prerequisite_path) do if p_node.x == candidate.x and p_node.y == candidate.y then is_safe = false break end end end if is_safe then for _, other_key in ipairs(all_key_positions) do if other_key.x ~= self_pos.x or other_key.y ~= self_pos.y then if manhattan_distance(candidate, other_key) < min_dist then is_safe = false break end end end end if is_safe and manhattan_distance(candidate, self_pos) > 0 then return candidate end end return path_to_target[2] or path_to_target[1] or self_pos end function find_distances_from(start_pos) local dists, q = {}, {{x=start_pos.x,y=start_pos.y,dist=0}} for y=0, CONFIG.MAP_H-1 do dists[y]={} end dists[start_pos.y][start_pos.x]=0 local head=1 while head <= #q do local curr = q[head]; head=head+1 for _,dir in ipairs({{0,-1},{0,1},{-1,0},{1,0}}) do local nx,ny = curr.x+dir[1], curr.y+dir[2] if mget(nx,ny) ~= CONFIG.SPRITES.WALL and not dists[ny][nx] then dists[ny][nx]=curr.dist+1 table.insert(q, {x=nx,y=ny,dist=curr.dist+1}) end end end return dists end function find_and_measure_dead_ends(goal_pos, dists_from_start) local dead_ends, start_pos = {}, {x=1,y=1} for y=1, CONFIG.MAP_H-2 do for x=1, CONFIG.MAP_W-2 do if mget(x,y)==CONFIG.SPRITES.EMPTY and count_neighbors(x,y)==1 then if (x~=goal_pos.x or y~=goal_pos.y) and (x~=start_pos.x or y~=start_pos.y) then local len = measure_path_to_junction(x,y) local dist = dists_from_start[y] and dists_from_start[y][x] or 1 local score = len * dist table.insert(dead_ends, {pos={x=x,y=y}, score=score}) end end end end return dead_ends end function find_furthest_tile(sx,sy) local q, visited = {{x=sx,y=sy,d=0}}, {} for y=0,CONFIG.MAP_H-1 do visited[y]={} end visited[sy][sx]=true local max_dist, furthest_tile = 0, {x=sx,y=sy} local head=1 while head <= #q do local curr = q[head]; head=head+1 if curr.d > max_dist then max_dist=curr.d furthest_tile={x=curr.x, y=curr.y} end for _,d in ipairs({{0,-1},{0,1},{-1,0},{1,0}}) do local nx,ny = curr.x+d[1], curr.y+d[2] if mget(nx,ny)==CONFIG.SPRITES.EMPTY and not visited[ny][nx] then visited[ny][nx]=true table.insert(q,{x=nx,y=ny,d=curr.d+1}) end end end return furthest_tile end function count_neighbors(x,y) local count=0 for _,d in ipairs({{0,-1},{0,1},{-1,0},{1,0}}) do if mget(x+d[1], y+d[2]) ~= CONFIG.SPRITES.WALL then count=count+1 end end return count end function measure_path_to_junction(sx,sy) local px,py, cx,cy, len = sx,sy, sx,sy, 0 while true do if count_neighbors(cx,cy)>2 or count_neighbors(cx,cy)==0 then return len end for _,d in ipairs({{0,-1},{0,1},{-1,0},{1,0}}) do local nx,ny = cx+d[1], cy+d[2] if mget(nx,ny)~=CONFIG.SPRITES.WALL and(nx~=px or ny~=py)then px,py=cx,cy; cx,cy=nx,ny; len=len+1; break end end if len > CONFIG.MAP_W * CONFIG.MAP_H then return 0 end end end -- Returns true if the maze is solvable (player can reach goal collecting keys in order) function is_maze_solvable() local settings = GAME.menu.settings local all_possible_keys = {} for i = 1, settings.key_count do table.insert(all_possible_keys, CONFIG.COLOR_DATA[i]) end -- Find positions of all items on the map local key_positions, door_positions = {}, {} local goal_pos = nil for y = 0, CONFIG.MAP_H - 1 do for x = 0, CONFIG.MAP_W - 1 do local tid = mget(x, y) for _, color in ipairs(all_possible_keys) do if tid == color.key then key_positions[color.name] = {x = x, y = y, color=color} end if tid == color.door then door_positions[color.name] = {x = x, y = y} end end if tid == CONFIG.SPRITES.GOAL then goal_pos = {x = x, y = y} end end end -- Must have a goal if not goal_pos then return false end -- For every door, its key must exist for color_name, _ in pairs(door_positions) do if not key_positions[color_name] then return false -- Door exists but key does not end end -- Determine the actual key collection order based on distance from start local key_info = {} local dists_from_start = find_distances_from({x=1, y=1}) for _, key_data in pairs(key_positions) do local dist = dists_from_start[key_data.y] and dists_from_start[key_data.y][key_data.x] or 9999 table.insert(key_info, {color_data = key_data.color, key_pos = {x=key_data.x, y=key_data.y}, dist = dist}) end table.sort(key_info, function(a, b) return a.dist < b.dist end) -- Simulate collecting keys and opening doors in the calculated order local curr_pos = {x = 1, y = 1} local keys_collected = {} for _, info in ipairs(key_info) do -- If this key exists, path to it if not find_path_with_doors(curr_pos, info.key_pos, keys_collected) then return false end keys_collected[info.color_data.name] = true end -- Path to goal from the start, assuming all necessary keys are collected if not find_path_with_doors({x = 1, y = 1}, goal_pos, keys_collected) then return false end return true end -- Like find_path, but respects doors and keys_collected function find_path_with_doors(p1, p2, keys_collected) local visited = {} for y = 0, CONFIG.MAP_H - 1 do visited[y] = {} end local q = {{x = p1.x, y = p1.y}} visited[p1.y][p1.x] = true local head = 1 while head <= #q do local curr = q[head]; head = head + 1 if curr.x == p2.x and curr.y == p2.y then return true end for _, d in ipairs({{0, -1}, {0, 1}, {-1, 0}, {1, 0}}) do local nx, ny = curr.x + d[1], curr.y + d[2] if nx >= 0 and nx < CONFIG.MAP_W and ny >= 0 and ny < CONFIG.MAP_H and not visited[ny][nx] then local tid = mget(nx, ny) local blocked = false if tid == CONFIG.SPRITES.WALL then blocked = true end for _, color in ipairs(CONFIG.COLOR_DATA) do if tid == color.door and not keys_collected[color.name] then blocked = true end end if not blocked then visited[ny][nx] = true table.insert(q, {x = nx, y = ny}) end end end end return false end