I've implemented tic-tac-toe in TypeScript before; now I'm porting it to Elixir. As before, this is pure game logic, no UI involved. I'm very new to Elixir, so any suggestions are appreciated.
The actual game logic:
defmodule MassivelyMultiplayerTtt.Game do
defstruct current_player: :player_x,
winning_player: :unfinished,
board: Tuple.duplicate(:empty, 9)
def make_move(game, cell_num) do
case elem(game.board, cell_num) do
:x ->
{:square_filled, game}
:o ->
{:square_filled, game}
:empty ->
if game.winning_player != :unfinished do
{:game_already_over, game}
else
game =
if game.current_player == :player_x do
%{game | board: put_elem(game.board, cell_num, :x), current_player: :player_o}
else
%{game | board: put_elem(game.board, cell_num, :o), current_player: :player_x}
end
game = check_for_end(game)
case game.winning_player do
:unfinished ->
{:waiting_for_move, game}
_ ->
{:game_finished, game}
end
end
end
end
defp check_for_end(game) do
case game.board do
{:x, :x, :x, _, _, _, _, _, _} ->
%{game | winning_player: :player_x}
{_, _, _, :x, :x, :x, _, _, _} ->
%{game | winning_player: :player_x}
{_, _, _, _, _, _, :x, :x, :x} ->
%{game | winning_player: :player_x}
{:x, _, _, :x, _, _, :x, _, _} ->
%{game | winning_player: :player_x}
{_, :x, _, _, :x, _, _, :x, _} ->
%{game | winning_player: :player_x}
{_, _, :x, _, _, :x, _, _, :x} ->
%{game | winning_player: :player_x}
{:x, _, _, _, :x, _, _, _, :x} ->
%{game | winning_player: :player_x}
{_, _, :x, _, :x, _, :x, _, _} ->
%{game | winning_player: :player_x}
{:o, :o, :o, _, _, _, _, _, _} ->
%{game | winning_player: :player_o}
{_, _, _, :o, :o, :o, _, _, _} ->
%{game | winning_player: :player_o}
{_, _, _, _, _, _, :o, :o, :o} ->
%{game | winning_player: :player_o}
{:o, _, _, :o, _, _, :o, _, _} ->
%{game | winning_player: :player_o}
{_, :o, _, _, :o, _, _, :o, _} ->
%{game | winning_player: :player_o}
{_, _, :o, _, _, :o, _, _, :o} ->
%{game | winning_player: :player_o}
{:o, _, _, _, :o, _, _, _, :o} ->
%{game | winning_player: :player_o}
{_, _, :o, _, :o, _, :o, _, _} ->
%{game | winning_player: :player_o}
board ->
if Enum.any?(Tuple.to_list(board), &match?(:empty, &1)) do
game
else
%{game | winning_player: :drawn}
end
end
end
end
Unit tests:
defmodule MassivelyMultiplayerTtt.GameTest do
use ExUnit.Case, async: true
import MassivelyMultiplayerTtt.Game
test "recognizes X's victory" do
# Arrange
game = %MassivelyMultiplayerTtt.Game{}
# Act
{_, game} = make_move(game, 0)
{_, game} = make_move(game, 6)
{_, game} = make_move(game, 1)
{_, game} = make_move(game, 7)
{result, game} = make_move(game, 2)
# Assert
assert(result == :game_finished)
assert(game.winning_player == :player_x)
end
test "recognizes O's victory" do
# Arrange
game = %MassivelyMultiplayerTtt.Game{}
# Act
{_, game} = make_move(game, 0)
{_, game} = make_move(game, 6)
{_, game} = make_move(game, 1)
{_, game} = make_move(game, 7)
{_, game} = make_move(game, 5)
{result, game} = make_move(game, 8)
# Assert
assert(result == :game_finished)
assert(game.winning_player == :player_o)
end
test "recognizes a drawn game" do
# Arrange
game = %MassivelyMultiplayerTtt.Game{}
# Act
{_, game} = make_move(game, 0)
{_, game} = make_move(game, 1)
{_, game} = make_move(game, 3)
{_, game} = make_move(game, 4)
{_, game} = make_move(game, 7)
{_, game} = make_move(game, 6)
{_, game} = make_move(game, 2)
{_, game} = make_move(game, 5)
{result, game} = make_move(game, 8)
# Assert
assert(result == :game_finished)
assert(game.winning_player == :drawn)
end
test "returns an error when attempting to move to an already-filled cell" do
# Arrange
game = %MassivelyMultiplayerTtt.Game{}
# Act
{_, game} = make_move(game, 0)
{result, _} = make_move(game, 0)
# Assert
assert(result == :square_filled)
end
test "returns an error when attempting to move in an already completed game" do
# Arrange
game = %MassivelyMultiplayerTtt.Game{}
# Act
{_, game} = make_move(game, 0)
{_, game} = make_move(game, 6)
{_, game} = make_move(game, 1)
{_, game} = make_move(game, 7)
{_, game} = make_move(game, 2)
{result, _} = make_move(game, 5)
# Assert
assert(result == :game_already_over)
end
test "recognizes X's turn" do
# Arrange
game = %MassivelyMultiplayerTtt.Game{}
# Act
{_, game} = make_move(game, 0)
{result, game} = make_move(game, 1)
# Assert
assert(result == :waiting_for_move)
assert(game.current_player == :player_x)
end
test "recognizes O's turn" do
# Arrange
game = %MassivelyMultiplayerTtt.Game{}
# Act
{result, game} = make_move(game, 0)
# Assert
assert(result == :waiting_for_move)
assert(game.current_player == :player_o)
end
end