diff --git a/control.lua b/control.lua index a2986382..0c718d84 100644 --- a/control.lua +++ b/control.lua @@ -113,6 +113,9 @@ require 'modules.autostash' --require 'maps.choppy' --require 'maps.choppy_dx' +--![[Minesweeper?]]-- +--require 'maps.minesweeper.main' + --![[Infinite random dungeon with RPG]]-- --require 'maps.dungeons.main' --require 'maps.dungeons.tiered_dungeon' diff --git a/maps/minesweeper/functions.lua b/maps/minesweeper/functions.lua new file mode 100644 index 00000000..80310559 --- /dev/null +++ b/maps/minesweeper/functions.lua @@ -0,0 +1,136 @@ +--luacheck: ignore +local Public = {} +local LootRaffle = require "functions.loot_raffle" +local Get_noise = require "utils.get_noise" + +local ores = {} +ores[1] = {} +ores[2] = {} +for _ = 1, 15, 1 do table.insert(ores[1], "iron-ore") end +for _ = 1, 9, 1 do table.insert(ores[1], "coal") end +for _ = 1, 1, 1 do table.insert(ores[1], "crude-oil") end +for _ = 1, 6, 1 do table.insert(ores[2], "copper-ore") end +for _ = 1, 4, 1 do table.insert(ores[2], "stone") end +for _ = 1, 1, 1 do table.insert(ores[2], "uranium-ore") end + +function Public.kaboom(position) + local surface = game.surfaces[1] + surface.create_entity({name = "atomic-rocket", position = {position.x + 1, position.y + 1}, target = {position.x + 1, position.y + 1}, speed = 1, force = "minesweeper"}) +end + +function Public.is_minefield_tile(position, search_cell) + if search_cell then + for x = 0, 1, 1 do + for y = 0, 1, 1 do + local p = {x = position.x + x, y = position.y + y} + local tile = game.surfaces.nauvis.get_tile(p) + if tile.name == "nuclear-ground" then return true end + if tile.hidden_tile == "nuclear-ground" then return true end + end + end + return + end + + local tile = game.surfaces.nauvis.get_tile(position) + if tile.name == "nuclear-ground" then return true end + if tile.hidden_tile == "nuclear-ground" then return true end +end + +function Public.position_to_string(p) + return p.x .. "_" .. p.y +end + +function Public.position_to_cell_position(p) + local cell_position = {} + cell_position.x = math.floor(p.x * 0.5) * 2 + cell_position.y = math.floor(p.y * 0.5) * 2 + return cell_position +end + +function Public.get_terrain_tile(surface, position) + local seed = surface.map_gen_settings.seed + + local noise_1 = Get_noise("smol_areas", position, seed) + local noise_2 = Get_noise("cave_rivers", position, seed) + + local a = 0.08 + if math.floor((noise_1 * 8) % 5) ~= 0 then + if math.abs(noise_2) < a then + return "water-shallow" + end + end + + if noise_2 > 0 then return "sand-" .. math.floor((noise_2 * 10) % 3 + 1) end + return "grass-" .. math.floor((noise_2 * 10) % 3 + 1) +end + +function Public.disarm_reward(position) + local surface = game.surfaces[1] + local distance_to_center = math.sqrt(position.x ^ 2 + position.y ^ 2) + + surface.create_entity({ + name = "flying-text", + position = {position.x + 1, position.y + 1}, + text = "Mine disarmed!", + color = {r=0.98, g=0.66, b=0.22} + }) + + local tile_name = Public.get_terrain_tile(surface, position) + + if math.random(1, 3) ~= 1 or tile_name == "water-shallow" then return end + + if math.random(1, 8) == 1 then + local blacklist = LootRaffle.get_tech_blacklist(0.05 + distance_to_center * 0.00025) --max loot tier at ~4000 tiles + local item_stacks = LootRaffle.roll(math.random(16, 48) + math.floor(distance_to_center * 0.2), 16, blacklist) + local container = surface.create_entity({name = "crash-site-chest-" .. math.random(1, 2), position = {position.x + math.random(0, 1), position.y + math.random(0, 1)}, force = "neutral"}) + for _, item_stack in pairs(item_stacks) do container.insert(item_stack) end + container.minable = false + return + end + + local a, b = string.find(tile_name, "grass", 1, true) + local ore + if a then + ore = ores[1][math.random(1, #ores[1])] + else + ore = ores[2][math.random(1, #ores[2])] + end + + if ore == "crude-oil" then + surface.create_entity({name = "crude-oil", position = {position.x + 1, position.y + 1}, amount = 301000 + distance_to_center * 600}) + return + end + + for x = 0, 1, 1 do + for y = 0, 1, 1 do + local p = {x = position.x + x, y = position.y + y} + local tile_name = Public.get_terrain_tile(surface, p) + if tile_name ~= "water-shallow" then + surface.create_entity({name = ore, position = p, amount = 1000 + distance_to_center * 2}) + end + end + end +end + +function Public.uncover_terrain(position) + local surface = game.surfaces[1] + for x = 0, 1, 1 do + for y = 0, 1, 1 do + local p = {x = position.x + x, y = position.y + y} + local tile_name = Public.get_terrain_tile(surface, p) + local tile = surface.get_tile(p) + local mineable_tile_name = false + if tile.prototype.mineable_properties.minable then mineable_tile_name = tile.name end + + surface.set_hidden_tile(p, nil) + surface.set_tiles({{name = tile_name, position = p}}, true) + if math.random(1, 16) == 1 and tile_name == "water-shallow" then + surface.create_entity({name = "fish", position = p}) + end + + if mineable_tile_name then surface.set_tiles({{name = mineable_tile_name, position = p}}, true) end + end + end +end + +return Public diff --git a/maps/minesweeper/main.lua b/maps/minesweeper/main.lua new file mode 100644 index 00000000..ca075d85 --- /dev/null +++ b/maps/minesweeper/main.lua @@ -0,0 +1,582 @@ +--luacheck: ignore +--[[ +It's a Minesweeper thingy - MewMew +-- 1 to 9 = adjacent mines +-- 10 = mine +-- 11 = marked mine +]] -- + +require 'modules.satellite_score' + +local Functions = require 'maps.minesweeper.functions' +local Map_score = require 'comfy_panel.map_score' +local Map = require 'modules.map_info' +local Global = require 'utils.global' + +local minesweeper = {} +Global.register( + minesweeper, + function(tbl) + minesweeper = tbl + end +) + +local number_colors = { + [1] = {0, 0, 210}, + [2] = {0, 100, 0}, + [3] = {180, 0, 0}, + [4] = {0, 0, 120}, + [5] = {120, 0, 0}, + [6] = {0, 110, 110}, + [7] = {0, 0, 0}, + [8] = {125, 125, 125}, + [11] = {185, 0, 255} +} + +local rendering_tile_values = { + ['nuclear-ground'] = {offset = {0.6, -0.2}, zoom = 3, font = 'scenario-message-dialog'}, + ['stone-path'] = {offset = {0.54, -0.27}, zoom = 3, font = 'default-large'}, + ['concrete'] = {offset = {0.52, -0.28}, zoom = 3, font = 'default-large-bold'}, + ['hazard-concrete-left'] = {offset = {0.52, -0.28}, zoom = 3, font = 'default-large-bold'}, + ['hazard-concrete-right'] = {offset = {0.52, -0.28}, zoom = 3, font = 'default-large-bold'}, + ['refined-concrete'] = {offset = {0.54, -0.26}, zoom = 3, font = 'default-game'}, + ['refined-hazard-concrete-left'] = {offset = {0.54, -0.26}, zoom = 3, font = 'default-game'}, + ['refined-hazard-concrete-right'] = {offset = {0.54, -0.26}, zoom = 3, font = 'default-game'} +} + +local chunk_divide_vectors = {} +for x = 0, 30, 2 do + for y = 0, 30, 2 do + table.insert(chunk_divide_vectors, {x, y}) + end +end +local size_of_chunk_divide_vectors = #chunk_divide_vectors + +local chunk_vectors = {} +for x = -32, 32, 32 do + for y = -32, 32, 32 do + table.insert(chunk_vectors, {x, y}) + end +end + +local cell_update_vectors = {} +for x = -2, 2, 2 do + for y = -2, 2, 2 do + table.insert(cell_update_vectors, {x, y}) + end +end + +local cell_adjacent_vectors = {} +for x = -2, 2, 2 do + for y = -2, 2, 2 do + if x == 0 and y == 0 then + else + table.insert(cell_adjacent_vectors, {x, y}) + end + end +end + +local solving_vector_tables = {} +local i = 1 +for r = 3, 10, 1 do + solving_vector_tables[i] = {} + for x = r * -2, r * 2, 2 do + for y = r * -2, r * 2, 2 do + table.insert(solving_vector_tables[i], {x, y}) + end + end + i = i + 1 +end +local size_of_solving_vector_tables = #solving_vector_tables + +local function update_rendering(cell, position) + local tile = game.surfaces.nauvis.get_tile(position) + local tile_values = rendering_tile_values[tile.name] + if not tile_values then + tile_values = {offset = {0.6, -0.2}, zoom = 3, font = 'scenario-message-dialog'} + end + + if cell[2] then + rendering.destroy(cell[2]) + end + + local cell_value = cell[1] + + local color + if number_colors[cell_value] then + color = number_colors[cell_value] + else + color = {125, 125, 125} + end + + local p = {position.x + tile_values.offset[1], position.y + tile_values.offset[2]} + local text = cell_value + if cell_value == 11 then + text = 'X' + end + + cell[2] = + rendering.draw_text { + text = text, + surface = game.surfaces[1], + target = p, + color = color, + scale = tile_values.zoom, + font = tile_values.font, + draw_on_ground = true, + scale_with_zoom = false, + only_in_alt_mode = false + } +end + +local function get_adjacent_mine_count(position) + local count = 0 + for _, vector in pairs(cell_adjacent_vectors) do + local p = {x = position.x + vector[1], y = position.y + vector[2]} + local key = Functions.position_to_string(p) + local cell = minesweeper.cells[key] + if cell and cell[1] >= 10 then + count = count + 1 + end + end + return count +end + +local function kill_cell(position) + local key = Functions.position_to_string(position) + local cell = minesweeper.cells[key] + if cell and cell[2] then + rendering.destroy(cell[2]) + end + minesweeper.cells[key] = nil +end + +local function visit_cell(position) + local score_change = 0 + + if not Functions.is_minefield_tile(position, true) then + return score_change + end + + local key = Functions.position_to_string(position) + local cell = minesweeper.cells[key] + local cell_value_before_visit = false + + if cell then + if cell[1] == 10 then + Functions.kaboom(position) + score_change = -8 + cell[1] = -1 + for _, vector in pairs(cell_update_vectors) do + local p = {x = position.x + vector[1], y = position.y + vector[2]} + local key = Functions.position_to_string(p) + if minesweeper.cells[key] and minesweeper.cells[key][1] < 10 then + table.insert(minesweeper.visit_queue, {x = p.x, y = p.y}) + end + end + return score_change + end + + if cell[1] == 11 then + return score_change + end + + cell_value_before_visit = cell[1] + end + + if not cell then + minesweeper.cells[key] = {} + end + local cell = minesweeper.cells[key] + + cell[1] = get_adjacent_mine_count(position) + + if cell[1] == 0 then + for _, vector in pairs(cell_adjacent_vectors) do + local adjacent_position = {x = position.x + vector[1], y = position.y + vector[2]} + if Functions.is_minefield_tile(adjacent_position, true) then + local adjacent_key = Functions.position_to_string(adjacent_position) + if not minesweeper.cells[adjacent_key] then + minesweeper.cells[adjacent_key] = {} + end + local adjacent_cell = minesweeper.cells[adjacent_key] + local mine_count = get_adjacent_mine_count(adjacent_position) + adjacent_cell[1] = mine_count + update_rendering(adjacent_cell, adjacent_position) + if mine_count == 0 then + table.insert(minesweeper.visit_queue, {x = adjacent_position.x, y = adjacent_position.y}) + end + end + end + Functions.uncover_terrain(position) + kill_cell(position) + return score_change + end + + if cell_value_before_visit and cell_value_before_visit ~= cell[1] then + for _, vector in pairs(cell_adjacent_vectors) do + local adjacent_position = {x = position.x + vector[1], y = position.y + vector[2]} + local adjacent_key = Functions.position_to_string(adjacent_position) + local adjacent_cell = minesweeper.cells[adjacent_key] + if adjacent_cell and adjacent_cell[1] < 9 then + table.insert(minesweeper.visit_queue, {x = adjacent_position.x, y = adjacent_position.y}) + end + end + end + + update_rendering(cell, position) + + return score_change +end + +local function get_solving_vectors(position) + local distance_to_center = math.sqrt(position.x ^ 2 + position.y ^ 2) + local key = math.floor(distance_to_center * 0.005) + 1 + if key > size_of_solving_vector_tables then + key = size_of_solving_vector_tables + end + local solving_vectors = solving_vector_tables[key] + return solving_vectors +end + +local function are_mines_marked_around_target(position) + local marked_positions = {} + for _, vector in pairs(get_solving_vectors(position)) do + local p = {x = position.x + vector[1], y = position.y + vector[2]} + local key = Functions.position_to_string(p) + local cell = minesweeper.cells[key] + if cell then + if cell[1] == 10 then + return + end + if cell[1] == 11 then + table.insert(marked_positions, p) + end + end + end + return marked_positions +end + +local function solve_attempt(position) + local solved = false + for _, vector in pairs(get_solving_vectors(position)) do + local p = {x = position.x + vector[1], y = position.y + vector[2]} + local key = Functions.position_to_string(p) + local cell = minesweeper.cells[key] + if cell and cell[1] > 10 then + local marked_positions = are_mines_marked_around_target(p) + if marked_positions then + solved = true + for _, p in pairs(marked_positions) do + minesweeper.cells[Functions.position_to_string(p)][1] = -1 + visit_cell(p) + Functions.disarm_reward(p) + end + end + end + end + return solved +end + +local function mark_mine(entity, player) + local position = Functions.position_to_cell_position(entity.position) + local key = Functions.position_to_string(position) + local cell = minesweeper.cells[key] + local score_change = 0 + + --Success + if cell and cell[1] > 9 then + local surface = game.surfaces.nauvis + + if cell[1] == 10 then + score_change = 1 + end + + surface.create_entity( + { + name = 'flying-text', + position = entity.position, + text = 'Mine marked.', + color = {r = 0.98, g = 0.66, b = 0.22} + } + ) + + cell[1] = 11 + update_rendering(cell, position) + entity.destroy() + + local solved = solve_attempt(position) + if solved then + player.insert({name = 'stone-furnace', count = 1}) + return score_change + end + + local e = surface.create_entity({name = 'item-on-ground', position = {position.x + 1, position.y + 1}, stack = {name = 'stone-furnace', count = 1}}) + if e and e.valid then + e.to_be_looted = true + end + + return score_change + end + + --Trigger all adjacent mines when missplacing a disarming furnace. + for _, vector in pairs(cell_update_vectors) do + local p = {x = position.x + vector[1], y = position.y + vector[2]} + local key = Functions.position_to_string(p) + if minesweeper.cells[key] and minesweeper.cells[key][1] == 10 then + Functions.kaboom(p) + score_change = score_change - 8 + minesweeper.cells[key][1] = -1 + solve_attempt(p) + table.insert(minesweeper.visit_queue, {x = p.x, y = p.y}) + end + end + return score_change +end + +local function add_mines_to_chunk(left_top) + local distance_to_center = math.sqrt((left_top.x + 16) ^ 2 + (left_top.y + 16) ^ 2) + local base_mine_count = 40 + local max_mine_count = 128 + local mine_count = distance_to_center * 0.043 + base_mine_count + if mine_count > max_mine_count then + mine_count = max_mine_count + end + + local shuffle_index = {} + for i = 1, size_of_chunk_divide_vectors, 1 do + table.insert(shuffle_index, i) + end + table.shuffle_table(shuffle_index) + + -- place shuffled mines + for i = 1, mine_count, 1 do + local vector = chunk_divide_vectors[shuffle_index[i]] + local position = {x = left_top.x + vector[1], y = left_top.y + vector[2]} + local key = Functions.position_to_string(position) + minesweeper.cells[key] = {10} + minesweeper.active_mines = minesweeper.active_mines + 1 + end + + -- remove mines that would form a 3x3 block + for _, chunk_vector in pairs(chunk_vectors) do + local left_top_2 = {x = left_top.x + chunk_vector[1], y = left_top.y + chunk_vector[2]} + + for _, vector in pairs(chunk_divide_vectors) do + local position = {x = left_top_2.x + vector[1], y = left_top_2.y + vector[2]} + local key = Functions.position_to_string(position) + local cell = minesweeper.cells[key] + if cell and cell[1] == 10 then + if get_adjacent_mine_count(position) == 8 then + --if cell[2] then rendering.destroy(cell[2]) end + minesweeper.cells[key] = nil + end + end + end + --[[ + for _, vector in pairs(chunk_divide_vectors) do + local position = {x = left_top_2.x + vector[1], y = left_top_2.y + vector[2]} + local key = Functions.position_to_string(position) + local cell = minesweeper.cells[key] + if cell then update_rendering(cell, position) end + end + ]] + end +end + +local function on_chunk_generated(event) + local left_top = event.area.left_top + if event.surface.index ~= 1 then + return + end + + local tiles = {} + for x = 0, 31, 1 do + for y = 0, 31, 1 do + table.insert(tiles, {name = 'nuclear-ground', position = {x = left_top.x + x, y = left_top.y + y}}) + --table.insert(tiles, {name = Functions.get_terrain_tile(event.surface, {x = left_top.x + x, y = left_top.y + y}), position = {x = left_top.x + x, y = left_top.y + y}}) + end + end + event.surface.set_tiles(tiles, true) + + --surface.clear() will cause to trigger on_chunk_generated twice + local key = Functions.position_to_string(left_top) + if minesweeper.chunks[key] then + return + end + minesweeper.chunks[key] = true + + add_mines_to_chunk(left_top) +end + +local function on_player_changed_position(event) + local player = game.players[event.player_index] + if not Functions.is_minefield_tile(player.position) then + return + end + local cell_position = Functions.position_to_cell_position(player.position) + local score_change = visit_cell(cell_position) + if score_change < 0 then + solve_attempt(cell_position) + end + Map_score.set_score(player, Map_score.get_score(player) + score_change) +end + +local function deny_building(event) + local entity = event.created_entity + if not entity.valid then + return + end + if entity.name == 'entity-ghost' then + return + end + if Functions.is_minefield_tile(entity.position, true) then + if event.player_index then + local player = game.players[event.player_index] + if entity.position.x % 2 == 1 and entity.position.y % 2 == 1 and entity.name == 'stone-furnace' then + local score_change = mark_mine(entity, player) + Map_score.set_score(player, Map_score.get_score(player) + score_change) + return + end + player.insert({name = entity.name, count = 1}) + else + local inventory = event.robot.get_inventory(defines.inventory.robot_cargo) + inventory.insert({name = entity.name, count = 1}) + end + entity.destroy() + end +end + +local function on_built_entity(event) + deny_building(event) +end + +local function on_robot_built_entity(event) + deny_building(event) +end + +local function update_built_tiles(surface, tiles) + for _, placed_tile in pairs(tiles) do + local cell_position = Functions.position_to_cell_position(placed_tile.position) + local key = Functions.position_to_string(cell_position) + local cell = minesweeper.cells[key] + if cell and cell[1] ~= 10 then + update_rendering(cell, cell_position) + end + end +end + +local function on_player_built_tile(event) + update_built_tiles(game.surfaces[event.surface_index], event.tiles) +end + +local function on_robot_built_tile(event) + update_built_tiles(event.robot.surface, event.tiles) +end + +local function on_player_created(event) + local player = game.players[event.player_index] + player.insert({name = 'stone-furnace', count = 1}) +end + +local function on_player_respawned(event) + local player = game.players[event.player_index] + player.insert({name = 'stone-furnace', count = 1}) + + game.surfaces.nauvis.destroy_decoratives({name = 'nuclear-ground-patch'}) +end + +local function on_entity_died(event) + local entity = event.entity + if not entity.valid then + return + end + if entity.force.index ~= 2 then + return + end + local force = event.force + if not force then + return + end + if force.name ~= 'minesweeper' then + return + end + local revived_entity = entity.clone({position = entity.position}) + revived_entity.health = entity.prototype.max_health + entity.destroy() +end + +local function on_nth_tick() + for k, position in pairs(minesweeper.visit_queue) do + visit_cell(position) + table.remove(minesweeper.visit_queue, k) + break + end +end + +local function on_init() + game.create_force('minesweeper') + + global.custom_highscore.description = 'Minesweep rank:' + + local surface = game.surfaces[1] + local mgs = surface.map_gen_settings + mgs.water = 0 + mgs.cliff_settings = {cliff_elevation_interval = 0, cliff_elevation_0 = 0} + mgs.autoplace_controls = { + ['coal'] = {frequency = 0, size = 0, richness = 0}, + ['stone'] = {frequency = 0, size = 0, richness = 0}, + ['copper-ore'] = {frequency = 0, size = 0, richness = 0}, + ['iron-ore'] = {frequency = 0, size = 0, richness = 0}, + ['uranium-ore'] = {frequency = 0, size = 0, richness = 0}, + ['crude-oil'] = {frequency = 0, size = 0, richness = 0}, + ['trees'] = {frequency = 4, size = 0.5, richness = 0.1} + } + surface.map_gen_settings = mgs + surface.clear(true) + + minesweeper.chunks = {} + minesweeper.cells = {} + minesweeper.visit_queue = {} + minesweeper.player_data = {} + minesweeper.active_mines = 0 + minesweeper.disarmed_mines = 0 + minesweeper.triggered_mines = 0 + + local T = Map.Pop_info() + T.main_caption = 'Minesweeper' + T.sub_caption = '' + T.text = + table.concat( + { + 'Mechanical lifeforms once dominated this world.\n', + 'They have left long ago, leaving an inhabitable wasteland.\n', + 'It also seems riddled with buried explosives.\n\n', + 'Mark mines with your stone furnace.\n', + 'Marked mines are save to walk on.\n', + 'When enough mines in an area are marked,\n', + 'they will disarm and yield rewards!\n', + 'Faulty marking may trigger surrounding mines!!\n\n', + 'As you move away from spawn,\n', + 'mine density and radius required to disarm will increase.\n', + 'Crates will contain more loot and ore will have higher yield.\n\n', + 'The paint for the numerics does not work very well with the dirt.\n', + 'Laying some stone bricks or better may help.\n' + } + ) + T.main_caption_color = {r = 255, g = 125, b = 55} + T.sub_caption_color = {r = 0, g = 250, b = 150} +end + +local Event = require 'utils.event' +Event.on_init(on_init) +Event.on_nth_tick(2, on_nth_tick) +Event.add(defines.events.on_chunk_generated, on_chunk_generated) +Event.add(defines.events.on_player_changed_position, on_player_changed_position) +Event.add(defines.events.on_built_entity, on_built_entity) +Event.add(defines.events.on_entity_died, on_entity_died) +Event.add(defines.events.on_robot_built_entity, on_robot_built_entity) +Event.add(defines.events.on_player_created, on_player_created) +Event.add(defines.events.on_player_respawned, on_player_respawned) +Event.add(defines.events.on_robot_built_tile, on_robot_built_tile) +Event.add(defines.events.on_player_built_tile, on_player_built_tile)