Mix.install([
{:kino, "~> 0.6.1"}
])
defmodule Match.State.Player do
defstruct [:headX, :headY, :tail, :direction, :dead]
def step(%Match.State.Player{
headX: headX,
headY: headY,
tail: tail,
direction: direction,
dead: dead
})
when not dead do
%{x: offsetX, y: offsetY} =
case direction do
:up -> %{x: 0, y: -1}
:down -> %{x: 0, y: +1}
:right -> %{x: +1, y: 0}
:left -> %{x: -1, y: 0}
_ -> %{x: 0, y: 0}
end
newHeadX = headX + offsetX
newHeadY = headY + offsetY
dead = tail |> Enum.any?(fn %{x: tx, y: ty} -> newHeadX == tx and newHeadY == ty end)
tail = unless dead, do: stepTail(%{x: headX, y: headY}, tail), else: tail
%Match.State.Player{
headX: newHeadX,
headY: newHeadY,
tail: tail,
direction: direction,
dead: dead
}
end
def step(state) do
state
end
defp stepTail(new, [old | rest]) do
[new | stepTail(old, rest)]
end
defp stepTail(_new, []) do
# to drop last element in tail
[]
end
def kill(state) do
%Match.State.Player{state | dead: true}
end
end
ExUnit.start(autorun: false)
defmodule Match.State.Player.Tests do
use ExUnit.Case, async: true
require Match.State.Player
test "tail is incremented correctly" do
player = %Match.State.Player{
headX: 1,
headY: 10,
tail: [%{x: 1, y: 9}, %{x: 2, y: 9}, %{x: 2, y: 8}],
direction: :down,
dead: false
}
newPlayer = Match.State.Player.step(player)
assert is_list(newPlayer.tail)
assert [%{x: 1, y: 10}, %{x: 1, y: 9}, %{x: 2, y: 9}] = newPlayer.tail
end
test "dead player is not incremented" do
player = %Match.State.Player{
headX: 1,
headY: 10,
tail: [%{x: 1, y: 9}, %{x: 2, y: 9}, %{x: 2, y: 8}],
direction: :down,
dead: true
}
newPlayer = Match.State.Player.step(player)
assert is_list(newPlayer.tail)
assert [%{x: 1, y: 9}, %{x: 2, y: 9}, %{x: 2, y: 8}] = newPlayer.tail
end
test "self-collision kills" do
player = %Match.State.Player{
headX: 0,
headY: 1,
tail: [%{x: 1, y: 1}, %{x: 1, y: 0}, %{x: 0, y: 0}],
direction: :up,
dead: false
}
newPlayer = Match.State.Player.step(player)
assert newPlayer.dead
end
end
ExUnit.run()
defmodule Match.State do
defstruct [:player, :fruitX, :fruitY, :width, :height]
def new(width, height) do
%{x: fruitX, y: fruitY} = get_new_fruit_location(width, height, [])
player = %Match.State.Player{headX: 3, headY: 3, direction: :up, tail: [], dead: false}
%Match.State{player: player, fruitX: fruitX, fruitY: fruitY, width: width, height: height}
end
def step(%Match.State{
player: player,
fruitX: fruitX,
fruitY: fruitY,
width: width,
height: height
}) do
newPlayer = Match.State.Player.step(player)
newPlayer =
if newPlayer.headX < 0 or newPlayer.headX > width or newPlayer.headY < 0 or
newPlayer.headY > height do
Match.State.Player.kill(newPlayer)
else
newPlayer
end
%{x: fruitX, y: fruitY, player: newPlayer} =
if fruitX == newPlayer.headX and fruitY == newPlayer.headY do
%{x: newFruitX, y: newFruitY} = get_new_fruit_location(width, height, player.tail)
newPlayer = %Match.State.Player{
newPlayer
| tail:
newPlayer.tail ++
[List.last(player.tail, %{x: newPlayer.headX, y: newPlayer.headY})]
}
%{x: newFruitX, y: newFruitY, player: newPlayer}
else
%{x: fruitX, y: fruitY, player: newPlayer}
end
%Match.State{player: newPlayer, fruitX: fruitX, fruitY: fruitY, width: width, height: height}
end
defp get_new_fruit_location(width, height, tail) do
# Select new X & Y, that are within the field but not already overlapping with the snake
newFruitX =
0..width
|> Enum.filter(fn x ->
not (tail
|> Enum.map(fn %{x: x, y: _y} -> x end)
|> Enum.any?(fn x2 -> x == x2 end))
end)
|> Enum.random()
newFruitY =
0..height
|> Enum.filter(fn y ->
not (tail
|> Enum.map(fn %{x: _x, y: y} -> y end)
|> Enum.any?(fn y2 -> y == y2 end))
end)
|> Enum.random()
%{x: newFruitX, y: newFruitY}
end
def to_svg(state) do
"""
<svg version="1.1"
viewBox="0 0 #{state.width + 1} #{state.height + 1}"
xmlns="http://www.w3.org/2000/svg">
<rect x="#{state.fruitX}" y="#{state.fruitY}" width="1" height="1" fill="red" />
<rect x="#{state.player.headX}" y="#{state.player.headY}" width="1" height="1" fill="blue" />
#{state.player.tail |> Enum.map(fn t -> '<rect x="#{t.x}" y="#{t.y}" width="1" height="1" fill="green" />' end) |> Enum.join("\n")}
</svg>
"""
end
end
Match.State.new(10, 10) |> Match.State.to_svg() |> Kino.Image.new(:svg)
frame = Kino.Frame.new()
frame
state = Match.State.new(10, 10)
Kino.Control.interval(500)
|> Kino.Control.stream()
|> Stream.take(5)
|> Enum.reduce_while(state, fn _counter, state ->
image = state |> Match.State.to_svg() |> Kino.Image.new(:svg)
Kino.Frame.render(frame, image)
state = Match.State.step(state)
if state.player.dead, do: {:halt, state}, else: {:cont, state}
end)
frame = Kino.Frame.new()
frame
state = Match.State.new(25, 25)
Kino.Control.interval(500)
|> Kino.Control.stream()
|> Stream.take(5)
|> Enum.reduce_while(state, fn _counter, state ->
image = state |> Match.State.to_svg() |> Kino.Image.new(:svg)
Kino.Frame.render(frame, image)
state = Match.State.step(state)
if state.player.dead, do: {:halt, state}, else: {:cont, state}
end)
defmodule Match.State.Player.InputControl do
def on_key(player, %{key: "w", type: :keydown}), do: %{player | direction: :up}
def on_key(player, %{key: "s", type: :keydown}), do: %{player | direction: :down}
def on_key(player, %{key: "a", type: :keydown}), do: %{player | direction: :left}
def on_key(player, %{key: "d", type: :keydown}), do: %{player | direction: :right}
def on_key(player, _), do: player
end
frame = Kino.Frame.new() |> Kino.render()
match = Match.State.new(25, 25)
# The keyboard control
keyboard = Kino.Control.keyboard([:status, :keydown]) |> Kino.render()
# The refresh interval
interval = Kino.Control.interval(500)
[keyboard, interval]
|> Kino.Control.stream()
# Wait for keyboard enable
|> Stream.drop_while(fn
%{type: :status, enabled: true} -> false
_event -> true
end)
# While keyboard is enabled
|> Stream.take_while(fn
%{type: :status, enabled: false} -> false
_event -> true
end)
# Update Game
|> Enum.reduce_while(match, fn event, match ->
match =
if event.type == :interval do
image = match |> Match.State.to_svg() |> Kino.Image.new(:svg)
Kino.Frame.render(frame, image)
Match.State.step(match)
else
if event.type == :keydown do
%Match.State{match | player: Match.State.Player.InputControl.on_key(match.player, event)}
else
match
end
end
if match.player.dead, do: {:halt, match}, else: {:cont, match}
end)