1
0
mirror of https://github.com/Refactorio/RedMew.git synced 2024-12-14 10:13:13 +02:00
RedMew/features/snake/game.lua
2019-07-03 19:33:04 +01:00

412 lines
10 KiB
Lua

local Global = require 'utils.global'
local Event = require 'utils.event'
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 pairs = 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({'snake.snake_destroyed', player.name, 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 - food_count + 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
break
::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
head.entity.active = false
tail_entity.active = true
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
entity.color = player.color
entity.active = false
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 =
Token.register(
function()
tick_snakes()
check_snakes_for_collisions()
destroy_dead_snakes()
spawn_food()
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 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
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 cord, pos = find_new_snake_position()
if not cord then
player.print({'snake.spawn_snake_fail'})
return
end
player.teleport(pos, board.surface)
player.create_character()
character = player.character
character.character_running_speed_modifier = -1
local queue = queue_new()
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 or 15
board.surface = surface
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 or 30
board.max_food = max_food or 6
make_board()
destroy_food()
spawn_food()
board.is_running = true
Event.add_removable_nth_tick(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
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
function Public.end_game()
for index, snake in pairs(snakes) do
destroy_snake(index, snake)
end
destroy_food()
Event.remove_removable_nth_tick(board.update_rate, tick)
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