From e277b916578b0c37992a25be313e0d8d916ff2e8 Mon Sep 17 00:00:00 2001 From: grilledham Date: Sat, 22 Jun 2019 22:36:04 +0100 Subject: [PATCH 01/17] priority_queue clean up --- utils/priority_queue.lua | 88 +++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/utils/priority_queue.lua b/utils/priority_queue.lua index bccd2358..85f07335 100644 --- a/utils/priority_queue.lua +++ b/utils/priority_queue.lua @@ -1,20 +1,45 @@ +local Debug = require 'utils.debug' +local is_closure = Debug.is_closure +local floor = math.floor + local PriorityQueue = {} -function PriorityQueue.new() - return {} -end - -local function default_comp(a, b) +local function default_comparator(a, b) return a < b end -local function HeapifyFromEndToStart(queue, comp) - comp = comp or default_comp - local pos = #queue +--- Min heap implementation of a priority queue. Smaller elements, as determined by the comparator, +-- have a higher priority. +-- @param comparator the comparator function used to compare elements +-- @usage +-- local PriorityQueue = require 'utils.priority_queue' +-- +-- local queue = PriorityQueue.new() +-- PriorityQueue.push(queue, 4) +-- PriorityQueue.push(queue, 7) +-- PriorityQueue.push(queue, 2) +-- +-- game.print(PriorityQueue.pop(queue)) -- 2 +-- game.print(PriorityQueue.pop(queue)) -- 4 +-- game.print(PriorityQueue.pop(queue)) -- 7 +function PriorityQueue.new(comparator) + if comparator == nil then + comparator = default_comparator + elseif is_closure(comparator) then + error('comparator cannot be a closure.', 2) + end + + return {_comparator = comparator} +end + +local function heapify_from_end_to_start(self) + local comparator = self._comparator + local pos = #self while pos > 1 do - local parent = bit32.rshift(pos, 1) -- integer division by 2 - if comp(queue[pos], queue[parent]) then - queue[pos], queue[parent] = queue[parent], queue[pos] + local parent = floor(pos / 2) + local a, b = self[pos], self[parent] + if comparator(a, b) then + self[pos], self[parent] = b, a pos = parent else break @@ -22,25 +47,26 @@ local function HeapifyFromEndToStart(queue, comp) end end -local function HeapifyFromStartToEnd(queue, comp) - comp = comp or default_comp +local function heapify_from_start_to_end(self) + local comparator = self._comparator local parent = 1 local smallest = 1 + local count = #self while true do local child = parent * 2 - if child > #queue then + if child > count then break end - if comp(queue[child], queue[parent]) then + if comparator(self[child], self[parent]) then smallest = child end child = child + 1 - if child <= #queue and comp(queue[child], queue[smallest]) then + if child <= count and comparator(self[child], self[smallest]) then smallest = child end if parent ~= smallest then - queue[parent], queue[smallest] = queue[smallest], queue[parent] + self[parent], self[smallest] = self[smallest], self[parent] parent = smallest else break @@ -48,27 +74,31 @@ local function HeapifyFromStartToEnd(queue, comp) end end -function PriorityQueue.size(queue) - return #queue +--- Returns the number of the number of elements in the priority queue. +function PriorityQueue.size(self) + return #self end -function PriorityQueue.push(queue, element, comp) - table.insert(queue, element) - HeapifyFromEndToStart(queue, comp) +-- Inserts an element into the priority queue. +function PriorityQueue.push(self, element) + self[#self + 1] = element + heapify_from_end_to_start(self) end -function PriorityQueue.pop(queue, comp) - local element = queue[1] +-- Removes and returns the highest priority element from the priority queue. +function PriorityQueue.pop(self) + local element = self[1] - queue[1] = queue[#queue] - queue[#queue] = nil - HeapifyFromStartToEnd(queue, comp) + self[1] = self[#self] + self[#self] = nil + heapify_from_start_to_end(self) return element end -function PriorityQueue.peek(queue) - return queue[1] +-- Returns, without removing, the highest priority element from the priority queue. +function PriorityQueue.peek(self) + return self[1] end return PriorityQueue From b426de3ec17b0da05c24b2331b69f9abf404c04c Mon Sep 17 00:00:00 2001 From: grilledham Date: Sat, 22 Jun 2019 22:36:13 +0100 Subject: [PATCH 02/17] task clean up --- utils/task.lua | 93 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 28 deletions(-) diff --git a/utils/task.lua b/utils/task.lua index a1d30ab9..1faaa835 100644 --- a/utils/task.lua +++ b/utils/task.lua @@ -9,38 +9,63 @@ local PriorityQueue = require 'utils.priority_queue' local Event = require 'utils.event' local Token = require 'utils.token' local ErrorLogging = require 'utils.error_logging' +local Global = require 'utils.global' + +local floor = math.floor +local log10 = math.log10 +local Token_get = Token.get +local pcall = pcall +local Queue_peek = Queue.peek +local Queue_pop = Queue.pop +local Queue_push = Queue.push +local PriorityQueue_peek = PriorityQueue.peek +local PriorityQueue_pop = PriorityQueue.pop +local PriorityQueue_push = PriorityQueue.push local Task = {} -global.callbacks = global.callbacks or PriorityQueue.new() -global.next_async_callback_time = -1 -global.task_queue = global.task_queue or Queue.new() -global.total_task_weight = 0 -global.task_queue_speed = 1 - -local function comp(a, b) +local function comparator(a, b) return a.time < b.time end -global.tpt = global.task_queue_speed +local callbacks = PriorityQueue.new(comparator) +local task_queue = Queue.new() +local primitives = { + next_async_callback_time = -1, + total_task_weight = 0, + task_queue_speed = 1, + task_per_tick = 1 +} + +Global.register( + {callbacks = callbacks, task_queue = task_queue, primitives = primitives}, + function(tbl) + callbacks = tbl.callbacks + task_queue = tbl.task_queue + primitives = tbl.primitives + end +) + local function get_task_per_tick() if game.tick % 300 == 0 then - local size = global.total_task_weight - global.tpt = math.floor(math.log10(size + 1)) * global.task_queue_speed - if global.tpt < 1 then - global.tpt = 1 + local size = primitives.total_task_weight + local task_per_tick = floor(log10(size + 1)) * primitives.task_queue_speed + if task_per_tick < 1 then + task_per_tick = 1 end + + primitives.task_per_tick = task_per_tick + return task_per_tick end - return global.tpt + return primitives.task_per_tick end local function on_tick() - local queue = global.task_queue for i = 1, get_task_per_tick() do - local task = Queue.peek(queue) + local task = Queue_peek(task_queue) if task ~= nil then -- result is error if not success else result is a boolean for if the task should stay in the queue. - local success, result = pcall(Token.get(task.func_token), task.params) + local success, result = pcall(Token_get(task.func_token), task.params) if not success then if _DEBUG then error(result) @@ -48,19 +73,18 @@ local function on_tick() log(result) ErrorLogging.generate_error_report(result) end - Queue.pop(queue) - global.total_task_weight = global.total_task_weight - task.weight + Queue_pop(task_queue) + primitives.total_task_weight = primitives.total_task_weight - task.weight elseif not result then - Queue.pop(queue) - global.total_task_weight = global.total_task_weight - task.weight + Queue_pop(task_queue) + primitives.total_task_weight = primitives.total_task_weight - task.weight end end end - local callbacks = global.callbacks - local callback = PriorityQueue.peek(callbacks) + local callback = PriorityQueue_peek(callbacks) while callback ~= nil and game.tick >= callback.time do - local success, result = pcall(Token.get(callback.func_token), callback.params) + local success, result = pcall(Token_get(callback.func_token), callback.params) if not success then if _DEBUG then error(result) @@ -69,8 +93,8 @@ local function on_tick() ErrorLogging.generate_error_report(result) end end - PriorityQueue.pop(callbacks, comp) - callback = PriorityQueue.peek(callbacks) + PriorityQueue_pop(callbacks) + callback = PriorityQueue_peek(callbacks) end end @@ -85,7 +109,7 @@ function Task.set_timeout_in_ticks(ticks, func_token, params) end local time = game.tick + ticks local callback = {time = time, func_token = func_token, params = params} - PriorityQueue.push(global.callbacks, callback, comp) + PriorityQueue_push(callbacks, callback) end --- Allows you to set a timer (in seconds) after which the tokened function will be run with params given as an argument @@ -109,8 +133,21 @@ end -- Ex. if the task is expected to repeat multiple times (ie. the function returns true and loops several ticks) function Task.queue_task(func_token, params, weight) weight = weight or 1 - global.total_task_weight = global.total_task_weight + weight - Queue.push(global.task_queue, {func_token = func_token, params = params, weight = weight}) + primitives.total_task_weight = primitives.total_task_weight + weight + Queue_push(task_queue, {func_token = func_token, params = params, weight = weight}) +end + +function Task.get_queue_speed() + return primitives.task_queue_speed +end + +function Task.set_queue_speed(value) + value = value or 1 + if value < 0 then + value = 0 + end + + primitives.task_queue_speed = value end Event.add(defines.events.on_tick, on_tick) From 3c5380dd32345d3ed3ec87ea5995d155386adec3 Mon Sep 17 00:00:00 2001 From: grilledham Date: Sat, 22 Jun 2019 22:42:27 +0100 Subject: [PATCH 03/17] made tick local --- utils/task.lua | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/utils/task.lua b/utils/task.lua index 1faaa835..ded5cd66 100644 --- a/utils/task.lua +++ b/utils/task.lua @@ -46,8 +46,8 @@ Global.register( end ) -local function get_task_per_tick() - if game.tick % 300 == 0 then +local function get_task_per_tick(tick) + if tick % 300 == 0 then local size = primitives.total_task_weight local task_per_tick = floor(log10(size + 1)) * primitives.task_queue_speed if task_per_tick < 1 then @@ -61,7 +61,9 @@ local function get_task_per_tick() end local function on_tick() - for i = 1, get_task_per_tick() do + local tick = game.tick + + for i = 1, get_task_per_tick(tick) do local task = Queue_peek(task_queue) if task ~= nil then -- result is error if not success else result is a boolean for if the task should stay in the queue. @@ -83,7 +85,7 @@ local function on_tick() end local callback = PriorityQueue_peek(callbacks) - while callback ~= nil and game.tick >= callback.time do + while callback ~= nil and tick >= callback.time do local success, result = pcall(Token_get(callback.func_token), callback.params) if not success then if _DEBUG then From 73c1515c8107ef8741836029c4d2e8fb810a31bb Mon Sep 17 00:00:00 2001 From: grilledham Date: Sat, 22 Jun 2019 22:53:58 +0100 Subject: [PATCH 04/17] change divide to multiply --- utils/priority_queue.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/priority_queue.lua b/utils/priority_queue.lua index 85f07335..05904997 100644 --- a/utils/priority_queue.lua +++ b/utils/priority_queue.lua @@ -36,7 +36,7 @@ local function heapify_from_end_to_start(self) local comparator = self._comparator local pos = #self while pos > 1 do - local parent = floor(pos / 2) + local parent = floor(pos * 0.5) local a, b = self[pos], self[parent] if comparator(a, b) then self[pos], self[parent] = b, a From 7a9c9f817089a7ecf00b66471e4b6db378b46c21 Mon Sep 17 00:00:00 2001 From: grilledham Date: Sun, 23 Jun 2019 10:07:17 +0100 Subject: [PATCH 05/17] improved priority queue docs --- utils/priority_queue.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/utils/priority_queue.lua b/utils/priority_queue.lua index 05904997..18ab9e8b 100644 --- a/utils/priority_queue.lua +++ b/utils/priority_queue.lua @@ -10,7 +10,8 @@ end --- Min heap implementation of a priority queue. Smaller elements, as determined by the comparator, -- have a higher priority. --- @param comparator the comparator function used to compare elements +-- @param comparator the comparator function used to compare elements, if nil the +-- deafult comparator is used. -- @usage -- local PriorityQueue = require 'utils.priority_queue' -- @@ -86,6 +87,7 @@ function PriorityQueue.push(self, element) end -- Removes and returns the highest priority element from the priority queue. +-- If the priority queue is empty returns nil. function PriorityQueue.pop(self) local element = self[1] @@ -97,6 +99,7 @@ function PriorityQueue.pop(self) end -- Returns, without removing, the highest priority element from the priority queue. +-- If the priority queue is empty returns nil. function PriorityQueue.peek(self) return self[1] end From 6f944fc3ecc72587dbbfaecf53eec1f08b8f1329 Mon Sep 17 00:00:00 2001 From: grilledham Date: Wed, 26 Jun 2019 13:19:08 +0100 Subject: [PATCH 06/17] apocalypse fix --- config.lua | 4 ++- features/apocalypse.lua | 72 +++++++++++++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/config.lua b/config.lua index dce8fbff..cb636b9f 100644 --- a/config.lua +++ b/config.lua @@ -357,7 +357,9 @@ global.config = { }, -- enables a command which allows for an end-game event apocalypse = { - enabled = true + enabled = true, + -- chance behemoth biters and spitters will double on death. + duplicate_chance = 0.05 }, -- gradually informs players of features such as chat, toasts, etc. player_onboarding = { diff --git a/features/apocalypse.lua b/features/apocalypse.lua index 39e50a62..f7d8dd48 100644 --- a/features/apocalypse.lua +++ b/features/apocalypse.lua @@ -5,15 +5,13 @@ local Global = require 'utils.global' local Command = require 'utils.command' local Toast = require 'features.gui.toast' local RS = require 'map_gen.shared.redmew_surface' -local HailHydra = require 'map_gen.shared.hail_hydra' local Color = require 'resources.color_presets' local Ranks = require 'resources.ranks' +local Event = require 'utils.event' +local random = math.random +local Config = require 'config' --- Constants -local hail_hydra_data = { - ['behemoth-spitter'] = {['behemoth-spitter'] = 0.01}, - ['behemoth-biter'] = {['behemoth-biter'] = 0.01} -} +local duplicate_chance = Config.apocalypse.duplicate_chance -- Local var local Public = {} @@ -35,6 +33,54 @@ Global.register( end ) +local name_map = {['behemoth-biter'] = true, ['behemoth-spitter'] = true} + +local biter_died_token = + Token.register( + function(event) + local entity = event.entity + if not entity.valid then + return + end + + local name = entity.name + if not name_map[name] then + return + end + + local force_name = entity.force.name + if force_name ~= 'enemy' then + return + end + + local surface = entity.surface + local create_entity = surface.create_entity + local position = entity.position + local spawn = {name = entity.name, force = 'enemy', position = position} + + create_entity(spawn) + + if random() > duplicate_chance then + return + end + + local spawn_position = surface.find_non_colliding_position(name, position, 8, 1) + if not spawn_position then + return + end + + spawn.position = spawn_position + create_entity(spawn) + end +) + +local aliens = { + 'behemoth-biter', + 'behemoth-biter', + 'behemoth-spitter', + 'behemoth-spitter' +} + local biter_spawn_token = Token.register( function() @@ -45,9 +91,6 @@ local biter_spawn_token = surface = RS.get_surface() player_force = game.forces.player - HailHydra.set_hydras(hail_hydra_data) - HailHydra.set_evolution_scale(1) - HailHydra.enable_hail_hydra() enemy_force.evolution_factor = 1 local p_spawn = player_force.get_spawn_position(surface) @@ -55,13 +98,6 @@ local biter_spawn_token = local create_entity = surface.create_entity - local aliens = { - 'behemoth-biter', - 'behemoth-biter', - 'behemoth-spitter', - 'behemoth-spitter' - } - for i = 1, #aliens do local spawn_pos = surface.find_non_colliding_position('behemoth-biter', p_spawn, 300, 1) if spawn_pos then @@ -72,6 +108,8 @@ local biter_spawn_token = group.set_command({type = defines.command.attack_area, destination = {0, 0}, radius = 500}) Toast.toast_all_players(500, {'apocalypse.toast_message'}) + + Event.add_removable(defines.events.on_entity_died, biter_died_token) end ) @@ -97,7 +135,7 @@ function Public.begin_apocalypse(_, player) primitives.apocalypse_now = true game.print({'apocalypse.apocalypse_begins'}, Color.pink) - Task.set_timeout(15, biter_spawn_token, {}) + Task.set_timeout(1, biter_spawn_token, {}) end Command.add( From dd0930ce215d71d7f00beb0ba686ac320b43c1bb Mon Sep 17 00:00:00 2001 From: grilledham Date: Wed, 3 Jul 2019 12:35:34 +0100 Subject: [PATCH 07/17] extra queue functions --- utils/queue.lua | 51 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/utils/queue.lua b/utils/queue.lua index 1907c30b..881cbd23 100644 --- a/utils/queue.lua +++ b/utils/queue.lua @@ -1,24 +1,39 @@ local Queue = {} function Queue.new() - local queue = {_head = 0, _tail = 0} + local queue = {_head = 1, _tail = 1} return queue end function Queue.size(queue) - return queue._tail - queue._head + return queue._head - queue._tail end function Queue.push(queue, element) local index = queue._head queue[index] = element - queue._head = index - 1 + queue._head = index + 1 +end + +--- Pushes the element such that it would be the next element pop'ed. +function Queue.push_to_end(queue, element) + local index = queue._tail - 1 + queue[index] = element + queue._tail = index end function Queue.peek(queue) return queue[queue._tail] end +function Queue.peek_start(queue) + return queue[queue._head - 1] +end + +function Queue.peek_index(queue, index) + return queue[queue._tail + index - 1] +end + function Queue.pop(queue) local index = queue._tail @@ -26,9 +41,37 @@ function Queue.pop(queue) queue[index] = nil if element then - queue._tail = index - 1 + queue._tail = index + 1 end + return element end +function Queue.to_array(queue) + local n = 1 + local res = {} + + for i = queue._tail, queue._head - 1 do + res[n] = queue[i] + n = n + 1 + end + + return res +end + +function Queue.pairs(queue) + local index = queue._tail + return function() + local element = queue[index] + + if element then + local old = index + index = index + 1 + return old, element + else + return nil + end + end +end + return Queue From 3cfbecca171a535b6fb764cb212dbc817a8ccf04 Mon Sep 17 00:00:00 2001 From: grilledham Date: Wed, 3 Jul 2019 12:35:38 +0100 Subject: [PATCH 08/17] snake --- features/snake/control.lua | 18 ++ features/snake/game.lua | 376 +++++++++++++++++++++++++++++++++++++ features/snake/gui.lua | 51 +++++ 3 files changed, 445 insertions(+) create mode 100644 features/snake/control.lua create mode 100644 features/snake/game.lua create mode 100644 features/snake/gui.lua diff --git a/features/snake/control.lua b/features/snake/control.lua new file mode 100644 index 00000000..3c48e361 --- /dev/null +++ b/features/snake/control.lua @@ -0,0 +1,18 @@ +local GameGui = require 'features.snake.gui' +local Game = require 'features.snake.game' + +local Public = {} + +function Public.start_game(surface, top_left_position, size, update_rate, max_food) + Game.start_game(surface, top_left_position, size, update_rate, max_food) + GameGui.show() +end + +function Public.end_game() + Game.end_game() + GameGui.destroy() +end + +remote.add_interface('snake', Public) + +return Public diff --git a/features/snake/game.lua b/features/snake/game.lua new file mode 100644 index 00000000..a57830c2 --- /dev/null +++ b/features/snake/game.lua @@ -0,0 +1,376 @@ +local Global = require 'utils.global' +local Task = require 'utils.task' +local Token = require 'utils.token' +local Queue = require 'utils.queue' + +local random = math.random +local queue_new = Queue.new +local push = Queue.push +local push_to_end = Queue.push_to_end +local pop = Queue.pop +local peek = Queue.peek +local peek_start = Queue.peek_start +local peek_index = Queue.peek_index +local queue_size = Queue.size +local queue_pairs = Queue.pairs + +local snakes = {} -- player_index -> snake_data {is_marked_for_destroy:bool, queue :Queue of {entity, cord} } +local board = { + size = 0, + surface = nil, + position = nil, + food_count = 0, + is_running = false, + update_rate = 30, + max_food = 6 +} +local cords_map = {} -- cords -> positions + +Global.register( + {snakes = snakes, board = board, cords_map = cords_map}, + function(tbl) + snakes = tbl.snakes + board = tbl.board + cords_map = tbl.cords_map + end +) + +local vectors = { + [0] = {x = 0, y = -1}, + [1] = {x = 0, y = -1}, + [2] = {x = 1, y = 0}, + [3] = {x = 1, y = 0}, + [4] = {x = 0, y = 1}, + [5] = {x = 0, y = 1}, + [6] = {x = -1, y = 0}, + [7] = {x = -1, y = 0} +} + +local function destroy_snake(index, snake) + for _, element in queue_pairs(snake.queue) do + local e = element.entity + if e and e.valid then + e.destroy() + end + end + snakes[index] = nil + + local player = game.get_player(index) + if not player or not player.valid then + return + end + + game.print(table.concat {player.name, ' has been destroyed with a score of ', queue_size(snake.queue), '.'}) +end + +local function destroy_dead_snakes() + for index, snake in pairs(snakes) do + if snake.is_marked_for_destroy then + destroy_snake(index, snake) + end + end +end + +local function spawn_food() + local size = board.size + local center = math.ceil(size / 2) + local surface = board.surface + local find_entity = surface.find_entity + + local food_count = board.food_count + local max_food = board.max_food + local tries = max_food + 10 + + while food_count < max_food and tries > 0 do + while tries > 0 do + tries = tries - 1 + local x, y = random(size), random(size) + + if x == center and y == center then + goto continue + end + + local pos = cords_map[x][y] + + local entity = find_entity('character', pos) or find_entity('compilatron', pos) + if entity then + goto continue + end + + entity = + surface.create_entity({name = 'compilatron', position = pos, force = 'neutral', direction = random(7)}) + entity.active = false + food_count = food_count + 1 + + do + break + end + + ::continue:: + end + end + + board.food_count = food_count +end + +local function destroy_food() + local position = board.position + local size = board.size + local food = + board.surface.find_entities_filtered( + { + name = 'compilatron', + area = {left_top = position, right_bottom = {position.x + size * 2, position.y + size * 2}} + } + ) + + for i = 1, #food do + local e = food[i] + if e.valid then + e.destroy() + end + end + + board.food_count = 0 +end + +local function get_new_head_cord(head_cord, direction) + local vector = vectors[direction] + local vec_x, vec_y = vector.x, vector.y + local x, y = head_cord.x + vec_x, head_cord.y + vec_y + + return x, y +end + +local function tick_snake(index, snake) + local player = game.get_player(index) + + if not player or not player.valid then + snake.is_marked_for_destroy = true + return + end + + local character = player.character + if not character or not character.valid then + snake.is_marked_for_destroy = true + return + end + + local surface = board.surface + local find_entity = surface.find_entity + local snake_queue = snake.queue + local snake_size = queue_size(snake_queue) + local head = peek_start(snake_queue) + local tail = peek(snake_queue) + local head_cord = head.cord + local tail_entity = tail.entity + local tail_cord = tail.cord + local size = board.size + + local walking_state = character.walking_state + walking_state.walking = true + local direction = walking_state.direction + local x, y = get_new_head_cord(head_cord, direction) + + if x <= 0 or x > size or y <= 0 or y > size then + snake.is_marked_for_destroy = true + tail_entity.destroy() + return + end + + local new_head_position = cords_map[x][y] + + if snake_size > 1 and find_entity('character', new_head_position) == peek_index(snake_queue, snake_size - 1).entity then + direction = (direction + 4) % 8 + walking_state.direction = direction + x, y = get_new_head_cord(head_cord, direction) + end + + if x <= 0 or x > size or y <= 0 or y > size then + snake.is_marked_for_destroy = true + tail_entity.destroy() + return + end + + new_head_position = cords_map[x][y] + + tail_entity.teleport(new_head_position) + tail.cord = {x = x, y = y} + + pop(snake_queue) + push(snake_queue, tail) + + player.character = nil + player.character = tail_entity + tail_entity.walking_state = walking_state + + local entity = find_entity('compilatron', new_head_position) + if entity and entity.valid then + entity.destroy() + + entity = + surface.create_entity {name = 'character', position = cords_map[tail_cord.x][tail_cord.y], force = 'player'} + entity.character_running_speed_modifier = -1 + push_to_end(snake_queue, {entity = entity, cord = tail_cord}) + + board.food_count = board.food_count - 1 + end +end + +local function tick_snakes() + for index, snake in pairs(snakes) do + tick_snake(index, snake) + end +end + +local function check_snakes_for_collisions() + local count_entities_filtered = board.surface.count_entities_filtered + for index, snake in pairs(snakes) do + if snake.is_marked_for_destroy then + goto continue + end + + if count_entities_filtered({name = 'character', position = peek_start(snake.queue).entity.position}) > 1 then + snake.is_marked_for_destroy = true + end + + ::continue:: + end +end + +local tick +tick = + Token.register( + function() + if not board.is_running then + return + end + + tick_snakes() + check_snakes_for_collisions() + destroy_dead_snakes() + spawn_food() + + Task.set_timeout_in_ticks(board.update_rate, tick) + end +) + +local function make_board() + local size = board.size + local position = board.position + local surface = board.surface + + local pos_x, pos_y = position.x, position.y + + for x = 1, size do + local col = {} + cords_map[x] = col + for y = 1, size do + col[y] = {pos_x + 2 * x - 0.5, pos_y + 2 * y - 0.5} + end + end + + size = size * 2 + local tiles = {} + + for x = 0, size do + for y = 0, size do + local pos = {pos_x + x, pos_y + y} + local tile_name + + if x == 0 or x == size or y == 0 or y == size then + tile_name = 'deepwater' + elseif x % 2 == 1 and y % 2 == 1 then + tile_name = 'grass-1' + else + tile_name = 'water' + end + + tiles[#tiles + 1] = {position = pos, name = tile_name} + end + end + + surface.set_tiles(tiles) +end + +local function new_snake(player) + if not board.is_running then + return + end + + if not player or not player.valid then + return + end + + if snakes[player.index] then + return + end + + local character = player.character + if character and character.valid then + character.destroy() + end + + local size = board.size + local center = math.ceil(size / 2) + + player.teleport(cords_map[center][center]) + + player.create_character() + character = player.character + character.character_running_speed_modifier = -1 + + local queue = queue_new() + push(queue, {entity = character, cord = {x = center, y = center}}) + local snake = {queue = queue} + + snakes[player.index] = snake +end + +local function new_game(surface, position, size, update_rate, max_food) + board.size = size + board.surface = surface + position.x = position.x + position.y = position.y + board.position = position + board.update_rate = update_rate + board.max_food = max_food + + make_board() + destroy_food() + spawn_food() + + board.is_running = true + + Task.set_timeout_in_ticks(board.update_rate, tick) +end + +local Public = {} + +function Public.start_game(surface, top_left_position, size, update_rate, max_food) + if board.is_running then + error('Snake game is already running you must end the game first.', 2) + end + + new_game(surface, top_left_position, size, update_rate or board.update_rate, max_food or board.max_food) +end + +function Public.end_game() + for i = 1, #snakes do + destroy_snake(i, snakes[i]) + end + + destroy_food() + + board.is_running = false +end + +function Public.new_snake(player) + new_snake(player) +end + +function Public.is_running() + return board.is_running +end + +return Public diff --git a/features/snake/gui.lua b/features/snake/gui.lua new file mode 100644 index 00000000..beefa592 --- /dev/null +++ b/features/snake/gui.lua @@ -0,0 +1,51 @@ +local Game = require 'features.snake.game' +local Gui = require 'utils.gui' +local Event = require 'utils.event' + +local Public = {} + +local main_button_name = Gui.uid_name() + +local function show_gui_for_player(player) + if not player or not player.valid then + return + end + + local top = player.gui.top + if not top[main_button_name] then + top.add {type = 'button', name = main_button_name, caption = 'Snake'} + end +end + +local function player_created(event) + if Game.is_running() then + local player = game.get_player(event.player_index) + show_gui_for_player(player) + end +end + +Event.add(defines.events.on_player_created, player_created) + +function Public.show() + for _, player in pairs(game.players) do + show_gui_for_player(player) + end +end + +function Public.destroy() + for _, player in pairs(game.players) do + local button = player.gui.top[main_button_name] + if button and button.valid then + button.destroy() + end + end +end + +Gui.on_click( + main_button_name, + function(event) + Game.new_snake(event.player) + end +) + +return Public From 8abffb30d39c04b943fb9db9b880ed1802a70c8f Mon Sep 17 00:00:00 2001 From: grilledham Date: Wed, 3 Jul 2019 15:45:34 +0100 Subject: [PATCH 09/17] updates --- features/snake/control.lua | 9 +++++ features/snake/game.lua | 69 +++++++++++++++++++++++++++++--------- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/features/snake/control.lua b/features/snake/control.lua index 3c48e361..e0c447bb 100644 --- a/features/snake/control.lua +++ b/features/snake/control.lua @@ -3,11 +3,20 @@ local Game = require 'features.snake.game' local Public = {} +--- Starts snake game. +-- Note when players join the game they will lose thier character. +-- @param surface Surface that the board is placed on. +-- @param top_left_position Position where board is placed. Defaults to {x = 1, y = 1}. +-- @param size size of board is board tiles. Note that the actaul size fo the board will be 2 * size in factorio tiles. Defaults to 15. +-- @param update_rate number of ticks between updates. Defaults to 30. +-- @param maximun food on the board. Defaults to 6. function Public.start_game(surface, top_left_position, size, update_rate, max_food) Game.start_game(surface, top_left_position, size, update_rate, max_food) GameGui.show() end +--- Ends the snake game. This will clean up any snake and food entities but will not restore the tiles nor +-- give players thier character back. function Public.end_game() Game.end_game() GameGui.destroy() diff --git a/features/snake/game.lua b/features/snake/game.lua index a57830c2..ab5a4acf 100644 --- a/features/snake/game.lua +++ b/features/snake/game.lua @@ -13,6 +13,8 @@ local peek_start = Queue.peek_start local peek_index = Queue.peek_index local queue_size = Queue.size local queue_pairs = Queue.pairs +local pairs = pairs +set_timeout_in_ticks = Task.set_timeout_in_ticks local snakes = {} -- player_index -> snake_data {is_marked_for_destroy:bool, queue :Queue of {entity, cord} } local board = { @@ -79,7 +81,7 @@ local function spawn_food() local food_count = board.food_count local max_food = board.max_food - local tries = max_food + 10 + local tries = max_food - food_count + 10 while food_count < max_food and tries > 0 do while tries > 0 do @@ -102,9 +104,7 @@ local function spawn_food() entity.active = false food_count = food_count + 1 - do - break - end + break ::continue:: end @@ -203,6 +203,8 @@ local function tick_snake(index, snake) player.character = nil player.character = tail_entity tail_entity.walking_state = walking_state + head.entity.active = false + tail_entity.active = true local entity = find_entity('compilatron', new_head_position) if entity and entity.valid then @@ -211,6 +213,8 @@ local function tick_snake(index, snake) entity = surface.create_entity {name = 'character', position = cords_map[tail_cord.x][tail_cord.y], force = 'player'} entity.character_running_speed_modifier = -1 + entity.color = player.color + entity.active = false push_to_end(snake_queue, {entity = entity, cord = tail_cord}) board.food_count = board.food_count - 1 @@ -251,7 +255,7 @@ tick = destroy_dead_snakes() spawn_food() - Task.set_timeout_in_ticks(board.update_rate, tick) + set_timeout_in_ticks(board.update_rate, tick) end ) @@ -293,6 +297,35 @@ local function make_board() surface.set_tiles(tiles) end +local function find_new_snake_position() + local size = board.size + local find_entity = board.surface.find_entity + + local min = math.min(4, size) + local max = math.max(1, size - 4) + + if min > max then + min, max = max, min + elseif min == max then + min = 1 + max = size + end + + local tries = 10 + + while tries > 0 do + tries = tries - 1 + + local x, y = random(min, max), random(min, max) + local pos = cords_map[x][y] + + local entity = find_entity('character', pos) or find_entity('compilatron', pos) + if not entity then + return {x = x, y = y}, pos + end + end +end + local function new_snake(player) if not board.is_running then return @@ -311,30 +344,34 @@ local function new_snake(player) character.destroy() end - local size = board.size - local center = math.ceil(size / 2) + local cord, pos = find_new_snake_position() - player.teleport(cords_map[center][center]) + if not cord then + player.print('Unable to spawn snake, please try again.') + return + end + player.teleport(pos) player.create_character() character = player.character character.character_running_speed_modifier = -1 local queue = queue_new() - push(queue, {entity = character, cord = {x = center, y = center}}) + push(queue, {entity = character, cord = cord}) local snake = {queue = queue} snakes[player.index] = snake end local function new_game(surface, position, size, update_rate, max_food) - board.size = size + board.size = size or 15 board.surface = surface - position.x = position.x - position.y = position.y + position = position or {x = 1, y = 1} + position.x = position.x or position[1] + position.y = position.y or position[2] board.position = position - board.update_rate = update_rate - board.max_food = max_food + board.update_rate = update_rate or 30 + board.max_food = max_food or 6 make_board() destroy_food() @@ -342,7 +379,7 @@ local function new_game(surface, position, size, update_rate, max_food) board.is_running = true - Task.set_timeout_in_ticks(board.update_rate, tick) + set_timeout_in_ticks(board.update_rate, tick) end local Public = {} @@ -356,7 +393,7 @@ function Public.start_game(surface, top_left_position, size, update_rate, max_fo end function Public.end_game() - for i = 1, #snakes do + for index, snake in pairs(snakes) do destroy_snake(i, snakes[i]) end From 9b46cebbb8e211c5af2ef54dfa9c85b31dd0c760 Mon Sep 17 00:00:00 2001 From: grilledham Date: Wed, 3 Jul 2019 15:50:19 +0100 Subject: [PATCH 10/17] localisation --- features/snake/game.lua | 4 ++-- features/snake/gui.lua | 2 +- locale/en/redmew_features.cfg | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/features/snake/game.lua b/features/snake/game.lua index ab5a4acf..af61ab53 100644 --- a/features/snake/game.lua +++ b/features/snake/game.lua @@ -62,7 +62,7 @@ local function destroy_snake(index, snake) return end - game.print(table.concat {player.name, ' has been destroyed with a score of ', queue_size(snake.queue), '.'}) + game.print({'snake.snake_destroyed', player.name, queue_size(snake.queue)}) end local function destroy_dead_snakes() @@ -347,7 +347,7 @@ local function new_snake(player) local cord, pos = find_new_snake_position() if not cord then - player.print('Unable to spawn snake, please try again.') + player.print({'snake.spawn_snake_fail'}) return end diff --git a/features/snake/gui.lua b/features/snake/gui.lua index beefa592..b222ccde 100644 --- a/features/snake/gui.lua +++ b/features/snake/gui.lua @@ -13,7 +13,7 @@ local function show_gui_for_player(player) local top = player.gui.top if not top[main_button_name] then - top.add {type = 'button', name = main_button_name, caption = 'Snake'} + top.add {type = 'button', name = main_button_name, caption = {'snake.name'}} end end diff --git a/locale/en/redmew_features.cfg b/locale/en/redmew_features.cfg index ce9f95f1..ff6dc0c6 100644 --- a/locale/en/redmew_features.cfg +++ b/locale/en/redmew_features.cfg @@ -155,3 +155,8 @@ ammo_count=Autofill ammo count invalid_ammo_count=ammo count must be a positive integer main_button_tooltip=Autofill settings frame_name=Autofill + +[snake] +name=Snake +spawn_snake_fail=Unable to spawn snake, please try again. +snake_destroyed=__1__ has been destroyed with a score of __2__. From ba05138427b8c0066588da14456a197c15bb0c04 Mon Sep 17 00:00:00 2001 From: grilledham Date: Wed, 3 Jul 2019 15:54:06 +0100 Subject: [PATCH 11/17] fix typos --- features/snake/control.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/snake/control.lua b/features/snake/control.lua index e0c447bb..52dd9bb2 100644 --- a/features/snake/control.lua +++ b/features/snake/control.lua @@ -7,7 +7,7 @@ local Public = {} -- Note when players join the game they will lose thier character. -- @param surface Surface that the board is placed on. -- @param top_left_position Position where board is placed. Defaults to {x = 1, y = 1}. --- @param size size of board is board tiles. Note that the actaul size fo the board will be 2 * size in factorio tiles. Defaults to 15. +-- @param size size of board in board tiles. Note that the actual size of the board will be 2 * size in factorio tiles. Defaults to 15. -- @param update_rate number of ticks between updates. Defaults to 30. -- @param maximun food on the board. Defaults to 6. function Public.start_game(surface, top_left_position, size, update_rate, max_food) From c2917c24cf8d63958a451e36eecf8d1e97f9057d Mon Sep 17 00:00:00 2001 From: grilledham Date: Wed, 3 Jul 2019 15:54:19 +0100 Subject: [PATCH 12/17] error on missing surface --- features/snake/game.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/features/snake/game.lua b/features/snake/game.lua index af61ab53..02260d0f 100644 --- a/features/snake/game.lua +++ b/features/snake/game.lua @@ -389,6 +389,10 @@ function Public.start_game(surface, top_left_position, size, update_rate, max_fo error('Snake game is already running you must end the game first.', 2) end + if not surface then + error('Surface must be set.', 2) + end + new_game(surface, top_left_position, size, update_rate or board.update_rate, max_food or board.max_food) end From e33b77e1bbe1d1884b295ebcf5c88376e3e4f8c6 Mon Sep 17 00:00:00 2001 From: grilledham Date: Wed, 3 Jul 2019 16:52:26 +0100 Subject: [PATCH 13/17] fix travis errors --- features/snake/game.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/snake/game.lua b/features/snake/game.lua index 02260d0f..a432b0e4 100644 --- a/features/snake/game.lua +++ b/features/snake/game.lua @@ -14,7 +14,7 @@ local peek_index = Queue.peek_index local queue_size = Queue.size local queue_pairs = Queue.pairs local pairs = pairs -set_timeout_in_ticks = Task.set_timeout_in_ticks +local set_timeout_in_ticks = Task.set_timeout_in_ticks local snakes = {} -- player_index -> snake_data {is_marked_for_destroy:bool, queue :Queue of {entity, cord} } local board = { @@ -398,7 +398,7 @@ end function Public.end_game() for index, snake in pairs(snakes) do - destroy_snake(i, snakes[i]) + destroy_snake(index, snake) end destroy_food() From dabadd8ac283859ec85722efc7c70c0d1a4c50cc Mon Sep 17 00:00:00 2001 From: grilledham Date: Wed, 3 Jul 2019 19:27:39 +0100 Subject: [PATCH 14/17] teleport player to board surface --- features/snake/game.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/snake/game.lua b/features/snake/game.lua index a432b0e4..337c4cd7 100644 --- a/features/snake/game.lua +++ b/features/snake/game.lua @@ -351,7 +351,7 @@ local function new_snake(player) return end - player.teleport(pos) + player.teleport(pos, board.surface) player.create_character() character = player.character character.character_running_speed_modifier = -1 From f8ceeed9bba9380b190a6932db75812825d68f72 Mon Sep 17 00:00:00 2001 From: grilledham Date: Wed, 3 Jul 2019 19:33:04 +0100 Subject: [PATCH 15/17] use n_th_tick instead of set_timeout --- features/snake/game.lua | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/features/snake/game.lua b/features/snake/game.lua index 337c4cd7..adf8d641 100644 --- a/features/snake/game.lua +++ b/features/snake/game.lua @@ -1,5 +1,5 @@ local Global = require 'utils.global' -local Task = require 'utils.task' +local Event = require 'utils.event' local Token = require 'utils.token' local Queue = require 'utils.queue' @@ -14,7 +14,6 @@ local peek_index = Queue.peek_index local queue_size = Queue.size local queue_pairs = Queue.pairs local pairs = pairs -local set_timeout_in_ticks = Task.set_timeout_in_ticks local snakes = {} -- player_index -> snake_data {is_marked_for_destroy:bool, queue :Queue of {entity, cord} } local board = { @@ -242,20 +241,13 @@ local function check_snakes_for_collisions() end end -local tick -tick = +local tick = Token.register( function() - if not board.is_running then - return - end - tick_snakes() check_snakes_for_collisions() destroy_dead_snakes() spawn_food() - - set_timeout_in_ticks(board.update_rate, tick) end ) @@ -379,7 +371,7 @@ local function new_game(surface, position, size, update_rate, max_food) board.is_running = true - set_timeout_in_ticks(board.update_rate, tick) + Event.add_removable_nth_tick(board.update_rate, tick) end local Public = {} @@ -403,6 +395,8 @@ function Public.end_game() destroy_food() + Event.remove_removable_nth_tick(board.update_rate, tick) + board.is_running = false end From dd28df75028067ef5b31288740c9b8a7210fd8f3 Mon Sep 17 00:00:00 2001 From: grilledham Date: Wed, 3 Jul 2019 19:36:44 +0100 Subject: [PATCH 16/17] update docs --- features/snake/control.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/features/snake/control.lua b/features/snake/control.lua index 52dd9bb2..a48420a7 100644 --- a/features/snake/control.lua +++ b/features/snake/control.lua @@ -7,7 +7,8 @@ local Public = {} -- Note when players join the game they will lose thier character. -- @param surface Surface that the board is placed on. -- @param top_left_position Position where board is placed. Defaults to {x = 1, y = 1}. --- @param size size of board in board tiles. Note that the actual size of the board will be 2 * size in factorio tiles. Defaults to 15. +-- @param size size of board in board tiles. Note that the actual size of the board will be (2 * size) + 1 in +-- factorio tiles. Defaults to 15. -- @param update_rate number of ticks between updates. Defaults to 30. -- @param maximun food on the board. Defaults to 6. function Public.start_game(surface, top_left_position, size, update_rate, max_food) From 20513fea25754100bf26a9a947ae4fd8df0ed2f3 Mon Sep 17 00:00:00 2001 From: grilledham Date: Wed, 3 Jul 2019 19:45:05 +0100 Subject: [PATCH 17/17] require snake --- control.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/control.lua b/control.lua index dd0ca4b4..c6f895f0 100644 --- a/control.lua +++ b/control.lua @@ -148,6 +148,8 @@ if config.redmew_settings.enabled then require 'features.gui.redmew_settings' end +require 'features.snake.control' + -- Debug-only modules if _DEBUG then require 'features.scenario_data_manipulation'