2019-07-03 13:35:38 +02:00
|
|
|
local Global = require 'utils.global'
|
2019-07-03 20:33:04 +02:00
|
|
|
local Event = require 'utils.event'
|
2019-07-03 13:35:38 +02:00
|
|
|
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
|
2019-07-03 16:45:34 +02:00
|
|
|
local pairs = pairs
|
2019-07-03 13:35:38 +02:00
|
|
|
|
2019-10-11 00:28:40 +02:00
|
|
|
local Public = {}
|
|
|
|
|
2019-07-03 13:35:38 +02:00
|
|
|
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
|
|
|
|
|
2019-10-11 00:28:40 +02:00
|
|
|
player.set_controller{type = defines.controllers.spectator}
|
|
|
|
|
|
|
|
local score = queue_size(snake.queue)
|
|
|
|
game.print({'snake.snake_destroyed', player.name, score})
|
|
|
|
|
|
|
|
script.raise_event(Public.events.on_snake_player_died, {
|
|
|
|
player = player,
|
|
|
|
score = score
|
|
|
|
})
|
2019-07-03 13:35:38 +02:00
|
|
|
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
|
2019-07-03 16:45:34 +02:00
|
|
|
local tries = max_food - food_count + 10
|
2019-07-03 13:35:38 +02:00
|
|
|
|
|
|
|
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
|
2019-10-11 00:28:40 +02:00
|
|
|
entity.destructible = false
|
2019-07-03 13:35:38 +02:00
|
|
|
food_count = food_count + 1
|
|
|
|
|
2019-07-03 16:45:34 +02:00
|
|
|
break
|
2019-07-03 13:35:38 +02:00
|
|
|
|
|
|
|
::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
|
2019-07-03 16:45:34 +02:00
|
|
|
head.entity.active = false
|
|
|
|
tail_entity.active = true
|
2019-07-03 13:35:38 +02:00
|
|
|
|
|
|
|
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
|
2019-07-03 16:45:34 +02:00
|
|
|
entity.color = player.color
|
|
|
|
entity.active = false
|
2019-10-11 00:28:40 +02:00
|
|
|
entity.destructible = false
|
2019-07-03 13:35:38 +02:00
|
|
|
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
|
|
|
|
|
2019-07-03 20:33:04 +02:00
|
|
|
local tick =
|
2019-07-03 13:35:38 +02:00
|
|
|
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
|
|
|
|
|
2019-07-03 16:45:34 +02:00
|
|
|
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
|
|
|
|
|
2019-07-03 13:35:38 +02:00
|
|
|
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
|
|
|
|
|
2019-07-03 16:45:34 +02:00
|
|
|
local cord, pos = find_new_snake_position()
|
2019-07-03 13:35:38 +02:00
|
|
|
|
2019-07-03 16:45:34 +02:00
|
|
|
if not cord then
|
2019-07-03 16:50:19 +02:00
|
|
|
player.print({'snake.spawn_snake_fail'})
|
2019-07-03 16:45:34 +02:00
|
|
|
return
|
|
|
|
end
|
2019-07-03 13:35:38 +02:00
|
|
|
|
2019-07-03 20:27:39 +02:00
|
|
|
player.teleport(pos, board.surface)
|
2019-10-11 00:28:40 +02:00
|
|
|
player.set_controller{type = defines.controllers.god}
|
2019-07-03 13:35:38 +02:00
|
|
|
player.create_character()
|
|
|
|
character = player.character
|
|
|
|
character.character_running_speed_modifier = -1
|
2019-10-11 00:28:40 +02:00
|
|
|
character.destructible = false
|
2019-07-03 13:35:38 +02:00
|
|
|
|
|
|
|
local queue = queue_new()
|
2019-07-03 16:45:34 +02:00
|
|
|
push(queue, {entity = character, cord = cord})
|
2019-07-03 13:35:38 +02:00
|
|
|
local snake = {queue = queue}
|
|
|
|
|
|
|
|
snakes[player.index] = snake
|
|
|
|
end
|
|
|
|
|
|
|
|
local function new_game(surface, position, size, update_rate, max_food)
|
2019-07-03 16:45:34 +02:00
|
|
|
board.size = size or 15
|
2019-07-03 13:35:38 +02:00
|
|
|
board.surface = surface
|
2019-07-03 16:45:34 +02:00
|
|
|
position = position or {x = 1, y = 1}
|
|
|
|
position.x = position.x or position[1]
|
|
|
|
position.y = position.y or position[2]
|
2019-07-03 13:35:38 +02:00
|
|
|
board.position = position
|
2019-07-03 16:45:34 +02:00
|
|
|
board.update_rate = update_rate or 30
|
|
|
|
board.max_food = max_food or 6
|
2019-07-03 13:35:38 +02:00
|
|
|
|
|
|
|
make_board()
|
|
|
|
destroy_food()
|
|
|
|
spawn_food()
|
|
|
|
|
|
|
|
board.is_running = true
|
|
|
|
|
2019-07-03 20:33:04 +02:00
|
|
|
Event.add_removable_nth_tick(board.update_rate, tick)
|
2019-07-03 13:35:38 +02:00
|
|
|
end
|
|
|
|
|
2019-10-11 00:28:40 +02:00
|
|
|
Public = {
|
|
|
|
events = {
|
|
|
|
--[[
|
|
|
|
on_snake_player_died
|
|
|
|
Called when a player have died in a game of snake
|
|
|
|
Contains
|
|
|
|
name :: uint: Unique identifier of the event
|
|
|
|
tick :: uint: Tick the event was generated.
|
|
|
|
player :: LuaPlayer
|
|
|
|
score :: uint: Score reached
|
|
|
|
]]
|
|
|
|
on_snake_player_died = Event.generate_event_name('on_snake_player_died')
|
|
|
|
}
|
|
|
|
}
|
2019-07-03 13:35:38 +02:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2019-07-03 16:54:19 +02:00
|
|
|
if not surface then
|
|
|
|
error('Surface must be set.', 2)
|
|
|
|
end
|
|
|
|
|
2019-07-03 13:35:38 +02:00
|
|
|
new_game(surface, top_left_position, size, update_rate or board.update_rate, max_food or board.max_food)
|
|
|
|
end
|
|
|
|
|
|
|
|
function Public.end_game()
|
2019-07-03 16:45:34 +02:00
|
|
|
for index, snake in pairs(snakes) do
|
2019-07-03 17:52:26 +02:00
|
|
|
destroy_snake(index, snake)
|
2019-07-03 13:35:38 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
destroy_food()
|
|
|
|
|
2019-07-03 20:33:04 +02:00
|
|
|
Event.remove_removable_nth_tick(board.update_rate, tick)
|
|
|
|
|
2019-07-03 13:35:38 +02:00
|
|
|
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
|