Andre Henriques
e3b2e60ddc
All checks were successful
continuous-integration/drone/push Build is passing
880 lines
29 KiB
Elixir
880 lines
29 KiB
Elixir
defmodule ServerMacros do
|
|
@moduledoc """
|
|
This module defines some helper macros that are used within the server macro
|
|
"""
|
|
|
|
@doc """
|
|
This macro creates a wait loop to wait for the messages that are receive match what is inside the
|
|
loop
|
|
"""
|
|
defmacro create_loop(name, do: match_exp) do
|
|
function_name = :"#{name}_loop"
|
|
|
|
ast1 = quote do
|
|
{:timeout} -> :timeout
|
|
end
|
|
ast2 = quote do
|
|
value ->
|
|
# TODO check spelling
|
|
log("Disreguarding: #{inspect(value)}")
|
|
# Process.send_after(self(), value, t + 2000)
|
|
unquote(function_name)(v, t)
|
|
end
|
|
|
|
ast3 = ast1 ++ match_exp ++ ast2
|
|
|
|
quote do
|
|
defp unquote(function_name)(v, t) do
|
|
var!(v) = v
|
|
receive do
|
|
unquote(ast3)
|
|
after
|
|
t -> :timeout
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
this function tries to propose and on failure it calls the else block and
|
|
on success tries to match with the expressions on the do block
|
|
"""
|
|
defmacro try_propose(val, do: ready, else: recal_do) do
|
|
ast1 = quote do
|
|
{:timeout} -> unquote(recal_do)
|
|
{:abort} -> unquote(recal_do)
|
|
end
|
|
ast2 = quote do
|
|
{:decision, v} ->
|
|
var!(state) = apply_game(var!(state), v)
|
|
unquote(recal_do)
|
|
|
|
v ->
|
|
raise "Unknown message on try_propose #{inspect(v)}"
|
|
end
|
|
|
|
ast3 = ast1 ++ ready ++ ast2
|
|
|
|
quote do
|
|
v = Paxos.propose(var!(state).paxos, var!(state).instance, unquote(val), 1000)
|
|
case v do
|
|
unquote(ast3)
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
defmodule Server do
|
|
@moduledoc """
|
|
Contains the to run the server Code
|
|
"""
|
|
require ServerMacros
|
|
import ServerMacros
|
|
require Utils
|
|
import Utils
|
|
|
|
create_log 2
|
|
|
|
@doc """
|
|
Contains the start code for the server
|
|
"""
|
|
def start(name, participants) do
|
|
log("#{name} Starting server")
|
|
|
|
pid = spawn(Server, :init, [name, participants])
|
|
register_name(name, pid, false)
|
|
end
|
|
|
|
@doc """
|
|
Initializes the state and starts the paxos inspect and then it calls the run fn
|
|
"""
|
|
def init(name, participants) do
|
|
paxos = Paxos.start(alter_name(name, "_paxos"), Enum.map(participants, fn name -> alter_name(name, "_paxos") end), true)
|
|
state = %{
|
|
name: name,
|
|
procs: participants,
|
|
games: %{},
|
|
paxos: paxos,
|
|
instance: 0,
|
|
}
|
|
|
|
run(state)
|
|
end
|
|
|
|
runfn do
|
|
{:start_game, participants, pid_to_inform} ->
|
|
or_state is_list(participants) do
|
|
{state, game_id} = try_to_create_game(state, participants)
|
|
state = set_modifed(state, game_id)
|
|
safecast(pid_to_inform, {:start_game_ans, game_id})
|
|
state
|
|
end
|
|
|
|
{:get_game_state, game_id, pid_to_inform} ->
|
|
state = get_game_state(state, game_id, pid_to_inform)
|
|
set_modifed(state, game_id)
|
|
|
|
|
|
{:make_move, game_id, move, pid_to_inform} ->
|
|
try_to_play_checks(state, game_id, move, pid_to_inform)
|
|
|
|
end
|
|
|
|
# Checks if the user can play the move before starting the requesting the user to play
|
|
defp try_to_play_checks(state, game_id, move, pid_to_inform, repeat \\ false) do
|
|
cond do
|
|
state.games[game_id] == :not_playing_in_game ->
|
|
safecast(pid_to_inform, {:make_move, game_id, :not_playing})
|
|
state
|
|
|
|
state.games[game_id] == nil ->
|
|
if repeat do
|
|
safecast(pid_to_inform, {:make_move, game_id, :game_does_not_exist})
|
|
state
|
|
else
|
|
state = qurey_status(state)
|
|
try_to_play_checks(state, game_id, move, pid_to_inform, true)
|
|
end
|
|
|
|
true ->
|
|
state = qurey_status(state)
|
|
game = state.games[game_id]
|
|
cond do
|
|
is_finished(state, game_id) ->
|
|
{_, score} = state.games[game_id]
|
|
safecast(pid_to_inform, {:make_move, game_id, :game_finished, score})
|
|
state
|
|
game.modified ->
|
|
safecast(pid_to_inform, {:make_move, game_id, :player_moved_before, game.game_state, game.hand})
|
|
set_modifed(state, game_id)
|
|
true ->
|
|
cond do
|
|
not is_number(move) ->
|
|
safecast(pid_to_inform, {:make_move, game_id, :invalid_move})
|
|
state
|
|
move >= length(game.game_state) ->
|
|
safecast(pid_to_inform, {:make_move, game_id, :invalid_move})
|
|
state
|
|
true ->
|
|
try_to_play(state, game_id, move, pid_to_inform)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# Tries to propose to paxos the game action
|
|
defp try_to_play(state, game_id, move, pid_to_inform) do
|
|
name = state.name
|
|
|
|
new_hand = if state.games[game_id].hand[state.name] == :- do
|
|
Enum.at(state.games[game_id].game_state, move)
|
|
else
|
|
get_hand_for_game_state(state.games[game_id].game_state)
|
|
end
|
|
|
|
try_propose {:make_move, game_id, name, move, new_hand}
|
|
do
|
|
{:decision, {:make_move, ^game_id, ^name, ^move, ^new_hand}} ->
|
|
state = apply_game(state, {:make_move, game_id, name, move, new_hand})
|
|
|
|
if is_finished(state, game_id) do
|
|
{_, score} = state.games[game_id]
|
|
safecast(pid_to_inform, {:make_move, game_id, :game_finished, score})
|
|
else
|
|
game = state.games[game_id]
|
|
safecast(pid_to_inform, {:make_move, game_id, game.game_state, game.hand[state.name]})
|
|
end
|
|
|
|
set_modifed(state, game_id)
|
|
|
|
{:decision, {:make_move, ^game_id, new_name, new_move, new_new_hand}} ->
|
|
state = apply_game(state, {:make_move, game_id, new_name, new_move, new_new_hand})
|
|
|
|
if is_finished(state, game_id) do
|
|
{_, score} = state.games[game_id]
|
|
safecast(pid_to_inform, {:make_move, game_id, :game_finished, score})
|
|
else
|
|
game = state.games[game_id]
|
|
safecast(pid_to_inform, {:make_move, game_id, :player_moved_before, game.game_state, game.hand})
|
|
end
|
|
|
|
set_modifed(state, game_id)
|
|
else
|
|
try_to_play(state, game_id, move, pid_to_inform)
|
|
end
|
|
end
|
|
|
|
# Get the most recent game_state and return it to the player
|
|
defp get_game_state(state, game_id, pid_to_inform, repeat \\ false) do
|
|
cond do
|
|
state.games[game_id] == :not_playing_in_game ->
|
|
safecast(pid_to_inform, {:game_state, game_id, :not_playing})
|
|
state
|
|
|
|
state.games[game_id] == nil ->
|
|
if repeat do
|
|
safecast(pid_to_inform, {:game_state, game_id, :game_does_not_exist})
|
|
state
|
|
else
|
|
state = qurey_status(state)
|
|
get_game_state(state, game_id, pid_to_inform, true)
|
|
end
|
|
|
|
true ->
|
|
state = qurey_status(state)
|
|
if is_finished(state, game_id) do
|
|
{_, score} = state.games[game_id]
|
|
safecast(pid_to_inform, {:game_state, game_id, :game_finished, score})
|
|
else
|
|
game = state.games[game_id]
|
|
safecast(pid_to_inform, {:game_state, game_id, game.game_state, game.hand[state.name]})
|
|
end
|
|
state
|
|
end
|
|
end
|
|
|
|
# This generates a new hand based on the current game state
|
|
defp get_hand_for_game_state(game_state) do
|
|
r1 = Enum.random(0..100)
|
|
cond do
|
|
r1 <= 1 -> :b
|
|
r1 <= 5 -> :-
|
|
r1 <= 20 -> :+
|
|
true ->
|
|
mx = game_state |> Enum.filter(fn m -> m != :+ end) |> Enum.max()
|
|
mn = max(mx - 20, 1)
|
|
mx = max(mx - 2, 4)
|
|
Enum.random(mn..mx)
|
|
end
|
|
end
|
|
|
|
# This tries to create a game by sending the create message to paxos
|
|
defp try_to_create_game(state, participants) do
|
|
game_ids = Map.keys(state.games)
|
|
latest = Enum.at(Enum.sort(game_ids), length(game_ids) - 1)
|
|
new_game_id = if latest do latest else 0 end + 1
|
|
|
|
new_game_state = Enum.to_list(0..Enum.random(3..8)) |> Enum.map(fn _ -> Enum.random(1..4) end)
|
|
hand = Enum.reduce(participants, %{}, fn p, acc -> Map.put(acc, p, get_hand_for_game_state(new_game_state)) end)
|
|
|
|
try_propose {:start_game, new_game_id, participants, new_game_state, hand}
|
|
do
|
|
{:decision, {:start_game, ^new_game_id, ^participants, ^new_game_state, ^hand}} ->
|
|
state = apply_game(state, {:start_game, new_game_id, participants, new_game_state, hand})
|
|
{state, new_game_id}
|
|
else
|
|
try_to_create_game(state, participants)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Utils
|
|
#
|
|
|
|
# Checks if a game has been finished
|
|
defp is_finished(state, game) do
|
|
case state.games[game] do
|
|
{:finished, _} -> true
|
|
_ -> false
|
|
end
|
|
end
|
|
|
|
# Gets up to the most recent instance
|
|
defp qurey_status(state) do
|
|
v = Paxos.get_decision(state.paxos, state.instance, 100)
|
|
or_state v != nil do
|
|
state = apply_game(state, v)
|
|
qurey_status(state)
|
|
end
|
|
end
|
|
|
|
# Sets the modified flag
|
|
defp set_modifed(state, game, val \\ false) do
|
|
or_state not is_finished(state, game) and state.games[game] != :not_playing_in_game and state.games[game] != nil do
|
|
%{state | games: Map.put(state.games, game, %{state.games[game] | modified: val})}
|
|
end
|
|
end
|
|
|
|
#
|
|
# Apply Game States
|
|
#
|
|
|
|
# Calculates the index based on the incoming index and a move
|
|
defp get_index(indexed_game_state, spos, index) do
|
|
index = spos + index
|
|
len = length(indexed_game_state)
|
|
cond do
|
|
index < 0 ->
|
|
len - 1
|
|
index >= len ->
|
|
rem(index, len)
|
|
true ->
|
|
index
|
|
end
|
|
end
|
|
|
|
# This funcion looks at pluses and blackholes and chekes if they can be merged
|
|
# This funcion will pick a state like this [ 1, 2, :+, 2, 1 ] and create a new state [ 1, {:merged, 3} , 1] and then call expand merge which will expand the merge
|
|
# Note by this point the state is indexed so it looks like [{1, 0}, {:+, 1}, {1, 2}]
|
|
defp simplify_game_state_pluses([], indexed_game_state), do: {false, indexed_game_state}
|
|
defp simplify_game_state_pluses([{:+, i} | tl], indexed_game_state) do
|
|
before_i = get_index(indexed_game_state, i, -1)
|
|
after_i = get_index(indexed_game_state, i, 1)
|
|
|
|
if before_i != after_i do
|
|
{b, b_i} = Enum.at(indexed_game_state, before_i)
|
|
{a, a_i} = Enum.at(indexed_game_state, after_i)
|
|
|
|
if b == a do
|
|
case b do
|
|
:+ -> simplify_game_state_pluses(tl, indexed_game_state)
|
|
:b -> simplify_game_state_pluses(tl, indexed_game_state)
|
|
n ->
|
|
list =
|
|
indexed_game_state |>
|
|
Enum.map(fn {x, ti} -> if ti == i, do: {{:merged, n + 1}, i}, else: {x, ti} end) |>
|
|
Enum.filter(fn {_, ti} -> cond do
|
|
b_i == ti -> false
|
|
a_i == ti -> false
|
|
true -> true
|
|
end
|
|
end) |>
|
|
reindex()
|
|
{true, expand_merge(list)}
|
|
end
|
|
else
|
|
simplify_game_state_pluses(tl, indexed_game_state)
|
|
end
|
|
else
|
|
simplify_game_state_pluses(tl, indexed_game_state)
|
|
end
|
|
end
|
|
|
|
defp simplify_game_state_pluses([{:b, i} | tl], indexed_game_state) do
|
|
before_i = get_index(indexed_game_state, i, -1)
|
|
after_i = get_index(indexed_game_state, i, 1)
|
|
|
|
if before_i != after_i do
|
|
{b, b_i} = Enum.at(indexed_game_state, before_i)
|
|
{a, a_i} = Enum.at(indexed_game_state, after_i)
|
|
|
|
a = if is_atom(a) do 1 else a end
|
|
b = if is_atom(b) do 1 else b end
|
|
|
|
list =
|
|
indexed_game_state |>
|
|
Enum.map(fn {x, ti} -> if ti == i, do: {{:merged, trunc(:math.floor((a + b) / 2))}, i}, else: {x, ti} end) |>
|
|
Enum.filter(fn {_, ti} -> cond do
|
|
b_i == ti -> false
|
|
a_i == ti -> false
|
|
true -> true
|
|
end
|
|
end) |>
|
|
reindex()
|
|
{true, expand_merge(list)}
|
|
else
|
|
simplify_game_state_pluses(tl, indexed_game_state)
|
|
end
|
|
end
|
|
|
|
# Check if the item in an game is the substate merged
|
|
defp is_merged(item) do
|
|
case item do
|
|
{:merged, _} -> true
|
|
_ -> false
|
|
end
|
|
end
|
|
|
|
# This funcion expands merges
|
|
# With a state like this [1, {:merged, 3}, 1] it will create a game state like this [ {:merged, 4 } ]
|
|
# Note by this point the state is indexed so it looks like [{1, 0}, {:+, 1}, {1, 2}]
|
|
defp expand_merge(indexed_game_state) do
|
|
{{:merged, n}, i} = indexed_game_state |> Enum.find(fn {x, _} -> is_merged(x) end)
|
|
|
|
b_i = get_index(indexed_game_state, i, - 1)
|
|
a_i = get_index(indexed_game_state, i, + 1)
|
|
|
|
if b_i != a_i do
|
|
{b, b_i} = Enum.at(indexed_game_state, b_i)
|
|
{a, a_i} = Enum.at(indexed_game_state, a_i)
|
|
if a == b do
|
|
case b do
|
|
:+ -> indexed_game_state
|
|
{:merged, _} -> indexed_game_state
|
|
_ ->
|
|
indexed_game_state |>
|
|
Enum.map(fn {x, ti} -> if ti == i, do: {{:merged, max(n, a) + 1}, i}, else: {x, ti} end) |>
|
|
Enum.filter(fn {_, ti} -> cond do
|
|
b_i == ti -> false
|
|
a_i == ti -> false
|
|
true -> true
|
|
end
|
|
end) |>
|
|
reindex() |>
|
|
expand_merge()
|
|
end
|
|
else
|
|
indexed_game_state
|
|
end
|
|
else
|
|
indexed_game_state
|
|
end
|
|
end
|
|
|
|
# This funcion removes previous indexes and reindexs the state
|
|
defp reindex(list, flat \\ true) do
|
|
list = if flat do
|
|
list |> Enum.map(fn {n, _} -> n end)
|
|
else
|
|
list
|
|
end
|
|
|
|
[list, 0..(length(list) - 1)] |> Enum.zip()
|
|
end
|
|
|
|
# Removes all merged from the array
|
|
defp remove_merged(l), do: l |> Enum.map(fn x -> case x do
|
|
{:merged, n} -> n
|
|
x -> x
|
|
end end)
|
|
|
|
# This funcion recieves the game state after the move was done and tries to simplify the game
|
|
defp simplify_game_state(game_state) do
|
|
indexed_game_state =
|
|
game_state |>
|
|
reindex(false)
|
|
|
|
{repeat, indexed_game_state} =
|
|
indexed_game_state |>
|
|
Enum.filter(fn x -> case x do
|
|
{:+, _} -> true
|
|
{:b, _} -> true
|
|
_ -> false
|
|
end
|
|
end) |>
|
|
simplify_game_state_pluses(indexed_game_state)
|
|
|
|
if repeat do
|
|
indexed_game_state |>
|
|
Enum.map(fn {v, _} -> v end) |>
|
|
remove_merged() |>
|
|
simplify_game_state()
|
|
else
|
|
indexed_game_state |> Enum.map(fn {v, _} -> v end)
|
|
end
|
|
end
|
|
|
|
# This function tries to apply the make_move command
|
|
defp apply_game(state, {:make_move, game_id, player_name, pos_move, new_hand}) do
|
|
game = state.games[game_id]
|
|
case game do
|
|
{:finished, _} ->
|
|
raise "Game already finished"
|
|
:not_playing_in_game ->
|
|
%{state | instance: state.instance + 1 }
|
|
game ->
|
|
game_state = game.game_state
|
|
game_state = if game.hand[player_name] == :- do
|
|
List.delete_at(game_state, pos_move)
|
|
else
|
|
{b, e} = Enum.split(game_state, pos_move)
|
|
b ++ [ game.hand[player_name] ] ++ e
|
|
end
|
|
game_state = simplify_game_state(game_state)
|
|
hand = Map.put(game.hand, player_name, new_hand)
|
|
game = %{game| hand: hand, game_state: game_state, modified: true }
|
|
|
|
if length(game.game_state) > 16 do
|
|
%{state| games: Map.put(state.games, game_id, {
|
|
:finished,
|
|
game.game_state |>
|
|
Enum.filter(fn x -> not is_atom(x) end) |>
|
|
Enum.sum()}), instance: state.instance + 1
|
|
}
|
|
else
|
|
%{state| games: Map.put(state.games, game_id, game), instance: state.instance + 1}
|
|
end
|
|
end
|
|
end
|
|
|
|
# This function tries to apply the start_game command
|
|
defp apply_game(state, {:start_game, game_id, participants, new_game_state, hand}) do
|
|
cond do
|
|
state.games[game_id] ->
|
|
raise "Game Already Exists"
|
|
state.name in participants ->
|
|
%{state |
|
|
games: Map.put(state.games, game_id, %{
|
|
game_state: new_game_state,
|
|
participants: participants,
|
|
hand: hand,
|
|
modified: true,
|
|
}),
|
|
instance: state.instance + 1
|
|
}
|
|
true ->
|
|
%{state |
|
|
games: Map.put(state.games, game_id, :not_playing_in_game),
|
|
instance: state.instance + 1,
|
|
}
|
|
end
|
|
end
|
|
|
|
defp apply_game(_, _), do: raise "Do not know how to apply game state"
|
|
|
|
############
|
|
# Interface
|
|
############
|
|
|
|
# handles responses from the start game request
|
|
create_loop :start_game do
|
|
{:start_game_ans, game_id} ->
|
|
log("Started a game #{game_id}")
|
|
{:start_game, game_id}
|
|
end
|
|
|
|
@doc """
|
|
Requests the server to start a game
|
|
"""
|
|
def start_game(name, participants) do
|
|
safecast(name, {:start_game, participants, self()})
|
|
start_game_loop(nil, 10000)
|
|
end
|
|
|
|
# handles responses from the get game state request
|
|
create_loop :get_game_state do
|
|
{:game_state, ^v, :not_playing} ->
|
|
log("Not Playing in that game")
|
|
{:not_playing}
|
|
{:game_state, ^v, :game_finished, score} ->
|
|
log("Game finsihed, #{score}")
|
|
{:game_finished, score}
|
|
{:game_state, ^v, game_state, hand} ->
|
|
log("Got game state, #{inspect(game_state)}, hand: #{inspect(hand)}")
|
|
{:state, game_state, hand}
|
|
{:game_state, ^v, :game_does_not_exist} ->
|
|
log("Got game does not exist")
|
|
{:not_exists}
|
|
end
|
|
|
|
@doc """
|
|
Requests the server to get the game state
|
|
"""
|
|
def get_game_state(name, game_id) do
|
|
safecast(name, {:get_game_state, game_id, self()})
|
|
get_game_state_loop(game_id, 10000)
|
|
end
|
|
|
|
# handles responses from the make move request
|
|
create_loop :make_move do
|
|
{:make_move, ^v, :game_does_not_exist} ->
|
|
log("Got game does not exist")
|
|
{:not_exists}
|
|
{:make_move, ^v, :not_playing} ->
|
|
log("Not Playing in that game")
|
|
{:not_playing}
|
|
{:make_move, ^v, :game_finished, score} ->
|
|
log("Game finsihed, #{score}")
|
|
{:game_finished, score}
|
|
{:make_move, ^v, :player_moved_before, game_state, hand} ->
|
|
log("Player moved_before, #{inspect(game_state)} #{inspect(hand)}")
|
|
{:player_moved_before, game_state, hand}
|
|
{:make_move, ^v, :invalid_move} ->
|
|
log("Invalid Move")
|
|
{:invalid_move}
|
|
{:make_move, ^v, game_state, hand} ->
|
|
log("Got game state, #{inspect(game_state)}, hand: #{inspect(hand)}")
|
|
{:state, game_state, hand}
|
|
end
|
|
|
|
@doc """
|
|
Requests to make a move
|
|
"""
|
|
def make_move(name, game_id, move) do
|
|
safecast(name, {:make_move, game_id, move, self()})
|
|
make_move_loop(game_id, 10000)
|
|
end
|
|
|
|
############
|
|
# Debug
|
|
############
|
|
|
|
@doc """
|
|
Quicky creates some servers it's useful in testing
|
|
"""
|
|
def spinup(number_of_participants) do
|
|
procs = Enum.to_list(1..number_of_participants) |> Enum.map(fn n -> :"p#{n}" end)
|
|
Enum.map(procs, fn proc -> Server.start(proc, procs) end)
|
|
end
|
|
|
|
@doc """
|
|
Quicky kills some servers it's useful in testing
|
|
"""
|
|
def kill (pids) do
|
|
pids |> Enum.map(fn m -> Process.exit(m, :kill) end)
|
|
end
|
|
end
|
|
|
|
defmodule Client do
|
|
@moduledoc """
|
|
This module handles displaying the game state in a nice to understand
|
|
way
|
|
"""
|
|
import Utils
|
|
require Utils
|
|
|
|
create_log 3
|
|
|
|
@doc """
|
|
This function plays the displays the game then waits for user input and displays the next state of the game
|
|
"""
|
|
def play_game(process, game_id) do
|
|
game = Server.get_game_state(process, game_id)
|
|
|
|
cont = case game do
|
|
:timeout ->
|
|
log("Could to not comunicate with the server")
|
|
true
|
|
{:not_exists} ->
|
|
log("Game does not exist")
|
|
true
|
|
{:not_playing} ->
|
|
log("Not Playing in that game")
|
|
true
|
|
{:game_finished, score} ->
|
|
log("Game finsihed, #{score}")
|
|
true
|
|
{:game_does_not_exist} ->
|
|
log("Got game does not exist")
|
|
true
|
|
{:state, game_state, hand} ->
|
|
game_state |>
|
|
to_name() |>
|
|
interpolate() |>
|
|
fill() |>
|
|
printpt(to_name(hand))
|
|
false
|
|
end
|
|
|
|
if not cont do
|
|
to_play = IO.gets("Type the number you want to play or q to exit: ")
|
|
to_play = to_play |> String.trim("\"") |> String.trim()
|
|
|
|
case to_play do
|
|
"q" ->
|
|
log("Bye Bye")
|
|
v ->
|
|
try do
|
|
{n, _} = Integer.parse(v)
|
|
|
|
res = Server.make_move(process, game_id, n)
|
|
case res do
|
|
:timeout ->
|
|
log("Could to not comunicate with the server")
|
|
{:not_exists} -> log("Game does not exist")
|
|
{:not_playing} -> log("Not Playing in that game")
|
|
{:game_finished, score} -> log("Game finsihed, #{score}")
|
|
{:game_does_not_exist} -> log("Got game does not exist")
|
|
{:player_moved_before, _, _} ->
|
|
log("Player Moved before you did please check the map and try again")
|
|
play_game(process, game_id)
|
|
{:invalid_move} -> raise "Invalid Move"
|
|
{:state, _, _} -> play_game(process, game_id)
|
|
end
|
|
rescue
|
|
_ ->
|
|
log("Please provide a valid number")
|
|
play_game(process, game_id)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
@doc """
|
|
This funcion waits for user input and then commands the server to act
|
|
|
|
Use the display_game funcion in a seperate terminal to see the game
|
|
"""
|
|
def control_game(process, game_id) do
|
|
to_play = IO.gets("Type the number you want to play or q to exit: ")
|
|
to_play = to_play |> String.trim("\"") |> String.trim()
|
|
|
|
case to_play do
|
|
"q" ->
|
|
log("Bye Bye")
|
|
v ->
|
|
try do
|
|
{n, _} = Integer.parse(v)
|
|
|
|
res = Server.make_move(process, game_id, n)
|
|
case res do
|
|
:timeout -> log("Could to not comunicate with the server")
|
|
{:not_exists} -> control_game(process, game_id)
|
|
{:not_playing} -> control_game(process, game_id)
|
|
{:game_finished, _} -> control_game(process, game_id)
|
|
{:player_moved_before, _, _} ->
|
|
log("Player Moved before you did please check the map and try again")
|
|
control_game(process, game_id)
|
|
{:invalid_move} -> raise "Invalid Move"
|
|
{:state, _, _} -> control_game(process, game_id)
|
|
end
|
|
rescue
|
|
_ ->
|
|
log("Please provide a valid number")
|
|
control_game(process, game_id)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
@doc """
|
|
This funcion tries to display the most recent version of the game
|
|
|
|
Use control_game to control game
|
|
"""
|
|
def display_game(process, game_id, temp_game \\ [], temp_hand \\ []) do
|
|
game = Server.get_game_state(process, game_id)
|
|
|
|
case game do
|
|
:timeout -> log("Could to not comunicate with the server")
|
|
{:not_exists} -> log("Game does not exist")
|
|
{:not_playing} -> log("Not Playing in that game")
|
|
{:game_finished, score} -> log("Game finsihed, #{score}")
|
|
{:game_does_not_exist} -> log("Got game does not exist")
|
|
{:state, game_state, hand} ->
|
|
if temp_game != game_state or temp_hand != hand do
|
|
# IO.ANSI.clear()
|
|
game_state |>
|
|
to_name() |>
|
|
interpolate() |>
|
|
fill() |>
|
|
printpt(to_name(hand))
|
|
Process.sleep(1000)
|
|
display_game(process, game_id, game_state, hand)
|
|
else
|
|
Process.sleep(1000)
|
|
display_game(process, game_id, temp_game, temp_hand)
|
|
end
|
|
end
|
|
end
|
|
|
|
# This funcion recieves some objects and transforms them into user friendly text
|
|
defp to_name(list) when is_list(list), do: list |> Enum.map(fn x -> to_name(x) end)
|
|
defp to_name(atom) when atom == :+, do: IO.ANSI.color_background(9) <> IO.ANSI.color(15) <> " + " <> IO.ANSI.reset()
|
|
defp to_name(atom) when atom == :-, do: IO.ANSI.color_background(21) <> IO.ANSI.color(15) <> " - " <> IO.ANSI.reset()
|
|
defp to_name(atom) when atom == :b, do: IO.ANSI.color_background(232) <> IO.ANSI.color(15) <> " b " <> IO.ANSI.reset()
|
|
defp to_name(atom) when is_atom(atom), do: atom
|
|
defp to_name(i) do
|
|
letter = [ "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", "Cn", "Nh", "Fl", "Mc", "Lv", "Ts", "Og" ] |> Enum.at(i - 1)
|
|
color = [46,45,138,19,11,140,8,47,57,159,72,48,55,35,251,188,107,110,21,11,156,134,128,179,140,234,14,90,206,7,249,209,253,123,192,165,234,136,198,208,43,34,215,127,23,250,177,237,124,202,229,
|
|
63,206,220,224,109,202,113,253,7,243,26,160,65,39,112,57,75, 252,82,213,186,68,243,134,100,226,48,90,134,208,102,25,106,72, 242,26,59,166,26,187,54,194,165,97,219,186,130,7,154,233,85, 130,67,43,200,90,60,148,49,161,110,247,116,223,159,132,132] |> Enum.at(i - 1) |> IO.ANSI.color()
|
|
color <> String.pad_leading("#{letter}", 3, " ") <> IO.ANSI.reset()
|
|
end
|
|
|
|
# Adds the index as an indicator and pads the index
|
|
defp interpolate(list) do
|
|
[list, 0..length(list) - 1] |>
|
|
Enum.zip() |>
|
|
Enum.reduce([], fn {v, i}, acc -> acc ++ [String.pad_leading("#{i}", 3, " "), v] end)
|
|
end
|
|
|
|
# This grows a list with between spaces every item of the interpolated list
|
|
# This allows the items to take as puch space in the possilble
|
|
defp grow_empty_list(t, i, acc) when i == 0, do: t ++ acc
|
|
defp grow_empty_list([], i, acc), do: grow_empty_list(acc, i, [])
|
|
defp grow_empty_list([h | tl], i, acc), do:
|
|
grow_empty_list(tl, i - 1, acc ++ [ h ++ [" "] ])
|
|
|
|
# This takes the list that was generated in the grow_empty_list function and merges it between every other item
|
|
defp fill(list) do
|
|
to_fill = 32 - length(list)
|
|
to_add = grow_empty_list(Enum.map(list, fn _ -> [] end), to_fill, [])
|
|
fill(list, to_add, [])
|
|
end
|
|
|
|
defp fill([], _, acc), do: acc
|
|
defp fill([hd | tail], [add_hd | add_tail], acc) do
|
|
fill(tail, add_tail ,acc ++ [hd] ++ add_hd)
|
|
end
|
|
|
|
# This functions prints the circle
|
|
defp printpt(game_state, hand), do: printpt(game_state, hand, 0)
|
|
defp printpt(_, _, i) when i > 16, do: nil
|
|
defp printpt(game_state, hand, i) do
|
|
res = case i do
|
|
0 ->
|
|
" xxx \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 1))
|
|
1 ->
|
|
" xxx yyy \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 0)) |>
|
|
String.replace("yyy", Enum.at(game_state, 2))
|
|
2 ->
|
|
" xxx yyy \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 31)) |>
|
|
String.replace("yyy", Enum.at(game_state, 3))
|
|
3 ->
|
|
" xxx yyy \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 30)) |>
|
|
String.replace("yyy", Enum.at(game_state, 4))
|
|
4 ->
|
|
" xxx yyy \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 29)) |>
|
|
String.replace("yyy", Enum.at(game_state, 5))
|
|
5 ->
|
|
" xxx yyy \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 28)) |>
|
|
String.replace("yyy", Enum.at(game_state, 6))
|
|
6 ->
|
|
" xxx yyy \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 27)) |>
|
|
String.replace("yyy", Enum.at(game_state, 7))
|
|
7 ->
|
|
" xxx yyy \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 26)) |>
|
|
String.replace("yyy", Enum.at(game_state, 8))
|
|
8 ->
|
|
" xxx zzz yyy \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 25)) |>
|
|
String.replace("yyy", Enum.at(game_state, 9)) |>
|
|
String.replace("zzz", hand)
|
|
9 ->
|
|
" xxx yyy \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 24)) |>
|
|
String.replace("yyy", Enum.at(game_state, 10))
|
|
10 ->
|
|
" xxx yyy \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 23)) |>
|
|
String.replace("yyy", Enum.at(game_state, 11))
|
|
11 ->
|
|
" xxx yyy \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 22)) |>
|
|
String.replace("yyy", Enum.at(game_state, 12))
|
|
12 ->
|
|
" xxx yyy \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 21)) |>
|
|
String.replace("yyy", Enum.at(game_state, 13))
|
|
13 ->
|
|
" xxx yyy \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 20)) |>
|
|
String.replace("yyy", Enum.at(game_state, 14))
|
|
14 ->
|
|
" xxx yyy \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 19)) |>
|
|
String.replace("yyy", Enum.at(game_state, 15))
|
|
15 ->
|
|
" xxx yyy \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 18)) |>
|
|
String.replace("yyy", Enum.at(game_state, 16))
|
|
16 ->
|
|
" xxx \n" |>
|
|
String.replace("xxx", Enum.at(game_state, 17))
|
|
end
|
|
|
|
IO.write("#{res}")
|
|
printpt(game_state, hand, i + 1)
|
|
end
|
|
end
|