A programing game inspired by Corewar / Darwin
--[[
# moonwar

The goal is to capture the most tiles by visiting them after a set amount of time.

## Arena

The arena is a 30*20 grid of tiles.

## Energy

Every core has a energy value from zero to one. At one, it can operate at
maximum core tick rate. At zero, it dies. Energy regenerates if the core
does no action in a tick. If a core splits, the energy is split between
the cores.

## Commands

-- Uses four (4) ticks.
-- leaves the cell the core is on empty and spawns a new core at the specified tile. If the target is not empty, the movement fails, which still takes 4 ticks.
function core.move(direction: "right" | "left" | "up" | "down") -> bool

-- Uses four (4) ticks.
function core.split(direction: "right" | "left" | "up" | "down", func, energy=0.5)-> bool

-- Waits for two (2) ticks, and kills core in specified direction, if there is one.
-- The attack is performed at the end of a simulation step.
-- Returns true if the attack killed a core.
function core.attack(direction: "right" | "left" | "up" | "down") -> bool

-- Adds the given data to the cores interal data array.
-- Takes one (1) tick.
function core.send(direction, data)

-- Returns information about the cell in the specified direction. The information is retrieved at the beginning of a simulation step.
-- Takes one (1) tick.
function core.inspect("right" | "left" | "up" | "down" | "self") -> {owned: nil/true/false, data}

-- Uses four (4) ticks. Returns the energy of the core.
function core.charge() -> float

-- Waits the given amount of world ticks.
function core.wait(ticks=1)

Example core:

```lua
function example(core)
	core.data = "yes"
	core.move "right"
	core.attack "right"
	return -- die
end
```

# Data

Each core has its own interal data which can be accessed and modified by itself and other cores.

# Simulation

Each world tick, each yielded core that has waited long enough is continued.
The cores are executed and the result is written to an intermediate state of the arena.
Conflicts in the arena (two cores on the same cell) are resolved by reverting
the movement. If that results in more conflicts, they are resolved recursively.
The intermediate state is then written to the world state.
]]

local Vector
Vector = function(x, y)
	return setmetatable({
		x = x,
		y = y,
	}, {
		__add = function(self, other)
			return Vector(self.x + other.x, self.y + other.y)
		end,
		__tostring = function(self)
			return string.format("%d, %d", self.x, self.y)
		end,
	})
end

local dirFromString = function(dir)
	if dir == "left" then
		return Vector(-1, 0)
	elseif dir == "right" then
		return Vector(1, 0)
	elseif dir == "up" then
		return Vector(0, -1)
	elseif dir == "down" then
		return Vector(0, 1)
	end
end

local Cell
local commands = {
	move = function(self, pos, arena, direction)
		if arena:hasCell(pos + dirFromString(direction)) then
			local from = arena:getCell(pos)
			arena:add(pos + dirFromString(direction), self, function()
				arena:add(pos, from)
			end)
			arena:remove(pos, self)
			arena:add(pos, Cell{owner=self.owner})
		end
		return 4
	end,
	split = function(self, pos, arena, direction, fun, energy)
		-- TODO: don't round, limit energy, type checking
		if arena:hasCell(pos + dirFromString(direction)) then
			energy = energy or 0.5
			local oldEnergy = self.energy
			local cell = Cell{owner=self.owner, core=fun, energy=self.energy * energy}
			arena:add(pos + dirFromString(direction), cell, function()
				self.energy = oldEnergy
			end)
			self.energy = self.energy * (1 - energy)
		end
		return 4
	end,
	attack = function(self, pos, arena, direction)
		-- TODO: more checks and return value
		if arena:hasCell(pos + dirFromString(direction)) then
			arena:add(pos + dirFromString(direction), Cell{owner=self.owner})
		end
		return 4
	end,
	send = function(_, pos, arena, data, direction)
		if arena:hasCell(pos) then
			table.insert(arena:getCell(pos + dirFromString(direction)).data, data)
		end
		return 1
	end,
	inspect = function(_, pos, arena, direction)
		if not arena:hasCell(pos + dirFromString(direction)) then
			return 1
		end
		-- TODO: better return value
		return 1, arena:getCell(pos + dirFromString(direction)).data
	end,
	charge = function(self)
		self.energy = self.energy + 4
		return 2, self.energy
	end,
	wait = function(_, amount)
		-- TODO: check amount, use world ticks
		return amount or 1
	end,
}

Cell = function(t)
	t = t or {}
	-- TODO: hardening
	local coreState = {
		data = {}
	}
	for command, fun in pairs(commands) do
		coreState[command] = function(...)
			return coroutine.yield(fun, {...})
		end
	end
	local cell = {
		waitTime = 0,
		energy = t.energy or 1,
		owner = t.owner,
		core = t.core and coroutine.create(t.core),
		data = t.core and coreState.data,
		returnValue = coreState,
	}
	return cell
end

local IntermediateArena = function(arena)
	local intermediate = {cells = {}}

	function intermediate:resolve(pos)
		local result
		for _, cell in ipairs(self.cells[pos.x][pos.y].cells) do
			if not result then
				result = cell
			elseif cell.core and result.core then
				return nil
			elseif cell.core and not result.core then
				result = cell
			end
		end
		return result or self:getCell(pos)
	end

	function intermediate:undo(pos)
		local cell = self.cells[pos.x][pos.y]
		for _, action in ipairs(cell.actions) do
			action()
		end
		cell.cells = {cell.original}
	end

	function intermediate:add(pos, cell, undo)
		if undo then
			table.insert(self.cells[pos.x][pos.y].actions, undo)
		end
		table.insert(self.cells[pos.x][pos.y].cells, cell)
	end

	function intermediate:remove(pos, cell)
		local cells = self.cells[pos.x][pos.y].cells
		for index, checkCell in ipairs(cells) do
			if cell == checkCell then
				table.remove(cells, index)
				break
			end
		end
	end

	function intermediate:getCell(pos)
		return self.cells[pos.x][pos.y].original
	end

	function intermediate:hasCell(pos)
		return self.cells[pos.x] and self.cells[pos.x][pos.y]
	end

	for x, row in ipairs(arena.cells) do
		intermediate.cells[x] = {}
		for y, cell in ipairs(row) do
			intermediate.cells[x][y] = {
				original = cell,
				cells = {cell},
				-- Undo actions.
				actions = {}
			}
		end
	end
	return intermediate
end

local Arena
Arena = function(width, height)
	local arena = {
		cells = {}
	}
	for x = 1, width do
		arena.cells[x] = {}
		for y = 1, height do
			arena.cells[x][y] = Cell()
		end
	end

	function arena:getCell(pos)
		return self.cells[pos.x][pos.y]
	end

	function arena:setCell(pos, cell)
		self.cells[pos.x][pos.y] = cell
	end

	function arena:spawn(x, y, name, core)
		self:setCell(Vector(x, y), Cell{core=core, owner=name})
	end

	function arena:simulate()
		local intermediate = IntermediateArena(self)
		for x, row in ipairs(self.cells) do
			for y, cell in ipairs(row) do
				if cell.core then
					cell.waitTime = cell.waitTime - 1
					if cell.waitTime <= 0 then
						if coroutine.status(cell.core) == "dead" then
							-- TODO: kill core
						else
							local _, command, args = coroutine.resume(cell.core, cell.returnValue)
							-- Validate that the command is not a malicious.
							local valid = false
							for _, fun in pairs(commands) do
								if fun == command then
									valid = true
								end
							end
							if valid then
								cell.waitTime, cell.returnValue = command(cell, Vector(x, y), intermediate, table.unpack(args))
								cell.waitTime = cell.waitTime * (1 - cell.energy) * 10
							end
						end
					end
				end
			end
		end
		-- Resolve conflicts by undoing the actions that caused them.

		local hasConflicts = true
		while hasConflicts do
			hasConflicts = false
			for x, row in ipairs(self.cells) do
				for y, _ in ipairs(row) do
					if not intermediate:resolve(Vector(x, y)) then
						hasConflicts = true
						intermediate:undo(Vector(x, y))
					end
				end
			end
		end
		-- Apply intermediate state.
		for x, row in ipairs(self.cells) do
			for y, _ in ipairs(row) do
				local pos = Vector(x, y)
				self:setCell(pos, intermediate:resolve(pos))
			end
		end
	end

	function arena:draw()
		for y = 1, height do
			for _ = 1, width*7-9 do
				io.write("-")
			end
			io.write("\n|")
			for x = 1, width do
				local owner = self:getCell(Vector(x, y)).owner
				--io.write(string.format("%.5s", tostring(owner):sub(5)))
				io.write(string.format("%-5s", owner or "                    "):sub(1, 5))
				io.write("|")
			end
			io.write("\n|")
			for x = 1, width do
				local core = self:getCell(Vector(x, y)).core
				io.write(core and "Core " or "     ")
				io.write("|")
			end
			io.write("\n|")
			for x = 1, width do
				local cell = self:getCell(Vector(x, y))
				io.write(cell.core and string.format("%-5f", cell.energy):sub(1, 5) or "     ")
				io.write("|")
			end
			io.write("\n")
		end
		for _ = 1, width*7-9 do
			io.write("-")
		end
		io.write("\n")
	end

	return arena
end

return Arena