Compare commits

..

14 Commits
latest ... main

Author SHA1 Message Date
d5383e67b2 chore: remove action
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-21 20:24:22 +00:00
05c09d10f3 fix comments
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-21 19:48:43 +00:00
134d958be8 fix comment
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-21 19:36:13 +00:00
0433210920 Merge branch 'main' of git.andr3h3nriqu3s.com:andr3/distributed_system_coursework
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-21 18:17:58 +00:00
7ff5414eba updated readme 2024-01-21 18:17:24 +00:00
074944ccb4 went back to markdown
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-20 23:01:23 +00:00
5740077954 removed .mv extension
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-20 22:58:31 +00:00
c803a3bb2b fixed read me 2024-01-20 22:57:44 +00:00
3cbf7b99af updated server and update to readme by seoeun
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-20 22:47:44 +00:00
e3b2e60ddc fixed compiler errors and updated read me
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-20 20:26:41 +00:00
2d20363870 updated readme
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-20 18:51:54 +00:00
8b35a1fc85 Added how to use instructins
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-20 17:50:23 +00:00
0f9f885cc0 did some fixes on the readme
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-19 21:29:45 +00:00
b21496dad6 worked on the readme
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-19 21:25:30 +00:00
4 changed files with 411 additions and 171 deletions

246
README.md
View File

@ -1 +1,245 @@
# TODO
Andre Goncalves Henriques (URN: 6644818), Seoeun Lee (URN: 6595203)
# Cooperative Application "Atomas" Game
The application that this project implements is a cooperative version of the game [Atomas](https://play.google.com/store/apps/details?id=com.sirnic.atomas&hl=en&gl=US&pli=1).
Atomas is a simple single player mobile phone game which allows players to combine atoms to make new atoms with higher atomic numbers.
The Server application implements a cooperative and simplified version of this game that allows players to play a game of Atomas in the same board at the same time.
The Server uses Paxos to guarantee that only one user can play at a time and that a player cannot make a move on a different game state than another player.
The `server.ex` file provides both `Server` Module that runs this service and a `Client` Module that can be used to display the game state in the terminal with nicer representation.
# API
The server provides the following API:
```elixir
{:start_game, participants, pid_to_inform}
{:get_game_state, game_id, pid_to_inform}
{:make_move, game_id, move, pid_to_inform}
```
## Important Concepts
The `game_id` is a number and is an identifier for the game. This can be a number as paxos guarantees that there cannot be two games with same number, as the server would have learned about that decision and would have incremented the number.
The `game_state` is a representation of a game state that corresponds to an array of numbers, and some special atoms, where the number represents the atomic number. The maximum length of this array is 16, so when an array gets bigger than that the game is over.
## Start Game
The `:start_game` message causes the server to create a new game.
The `:start_game` message takes the arguments `participants` and `pid_to_inform`.
- `participants` is a list of names of the servers that are participating in the game.
- `pid_to_inform` is a pid of the process that needs to be informed of the success or failure of this action.
For example, to request the start of a game, a user might do somethig like:
```elixir
send(server_pid, {:start_game, [:p0, :p1], self()})
# use recieve do to wait for the message
# Or, assuming the server was started with the name :p0
Server.start_game(:p0, [:p0, :p1])
```
The server will answer with:
```
{:start_game_ans, game_id}
```
This request will always create a new game (with the exception of crashes).
## Get Game State
The `:get_game_state` message causes the server to return the current state of a game.
The `:get_game_state` message takes the arguments as `game_id` and `pid_to_inform`.
- `game_id` is the game_id returned by the `:start_game` request
- `pid_to_inform` is a pid of the process that needs to be informed of the success or failure of this action.
For example, to request the current state of a game:
```elixir
send(server_pid, {:get_game_state, 1, self()})
# use 'recieve do' to wait for the message
# Or, assuming the server was started with the name ':p0'
Server.get_game_state(:p0, 1)
```
The server will answer with one of the following:
```elixir
{:game_state, game_id, :not_playing} # When the player is not playing in the game
{:game_state, game_id, :game_does_not_exist} # When the game was never created
{:game_state, game_id, :game_finished, score} # When the game is already over
{:game_state, game_id, game_state, hand} # When the player is in the game, and the game has started
# Returns the current game state of the game,
# and the current "atom" on the hand of the player
```
## Make a move
The `:make_move` message causes the server to try to play the move that the player is requesting.
The `:make_move` message takes the arguments as `game_id`, `move`, and `pid_to_inform`.
- `game_id` is the game_id returned by the `:start_game` request
- `move` is the position in the game state array where the play wants to insert their "atom"
- `pid_to_inform` is a pid of the process that needs to be informed of the success or failure of this action.
For example, to request the current state of a agame:
```elixir
send(server_pid, {:make_move, 1, 3, self()})
# use recieve do to wait for the message
# Or, assuming the server was started with the name :p0
Server.make_move(:p0, 1, 3)
```
The server will answer with one of the following:
```elixir
{:make_move, game_id, :not_playing} # When the player is not playing in the game
{:make_move, game_id, :game_does_not_exist} # When the game was never created
{:make_move, game_id, :game_finished, score} # When the game is already over
{:make_move, game_id, :invalid_move} # When the move requested by the player is an invalid move
{:make_move, game_id, :player_moved_before, game_state, hand} # When the player is in the game and the game has started,
# but another player has played the game before this request was made
# Returns the current game state of the game,
# and the current "atom" on the player's hand
{:make_move, game_id, game_state, hand} # When the player is in the game and the game has not finished
# and the move is valid
# Returns the current game state of the game,
# and the current "atom" on the hand of the player
```
# Properties
- If a game exists, then there was a process that creates it.
- Eventually, all processes will be in the same game state
- A player cannot make a move in a game state unless it's updated to the last game state
- If two players make a move at the same time in the same game, only one of the moves happens and the other player eventually gets notified
- If a player makes a valid move it will be evetually played
# Assumptions
- Processes won't behave in a byzantine way
- Only a minority of processes can fail
- Will run in a partial synchronous system
- When the game is created, the game state will have atoms in it, and the participants will have atoms in their hands
# Usage instructions
There are two ways of interacting with the server: via the API or the `Client` Module
## Notes
By default, all the logs are disabled to enable all the logs inside the paxos file. There is a module variable called `@min_print_level`.
Setting it to 0 enables all the logs
## API
Start a new iex session and compilie `paxos.ex` and `server.ex`:
```elixir
c "paxos.ex"; c "server.ex"
```
** Paxos needs to be compiled before the server as the server requires macros written in the paxos file **
Now start 3 instances of the server:
```elixir
procs = Enum.to_list(1..3) |> Enum.map(fn x -> :"p#{x}" end)
pids = procs |> Enum.map(fn x -> Server.start(x, procs) end)
```
After the 3 instances have been started, you can use Server helper functions to communicate with the Server:
```elixir
Server.start_game(:p1, [:p1, :p2])
Server.get_game_state(:p1, 1)
Server.make_move(:p1, 1, 0)
```
## Using the Client
Open 2 different terminals. One terminal will display the game, and the other one will be used to control the game
In the first terminal, start an iex session with a session name:
```
iex --sname c1
```
In the second terminal, start an iex session with a session name:
```
iex --sname c2
```
On the first terminal, connect the first terminal to the second terminal:
```elixir
# In c1
Node.connect(:"c2@<computer_name>")
```
On both terminals compile paxos and server:
```elixir
# In both run
c "paxos.ex"; c "server.ex"
```
Start the servers on the first terminal, create a game, and enter display mode:
```elixir
# In c1
procs = Enum.to_list(1..3) |> Enum.map(fn x -> :"p#{x}" end)
pids = procs |> Enum.map(fn x -> Server.start(x, procs) end)
Server.start_game(:p1, [:p1, :p2])
Client.display_game(:p1, 1)
```
You now should see something like:
```
0 H
1
Be
H
5 2
Be
Be Li
4 3
Be
```
The Center "Atom" is your hand, while the Letters arround are the "game_state" or "board",
the number represents where you can put the "Atom" in your hand
Start control mode on the second terminal by entering:
```elixir
# In c2
Client.control_game(:p1, 1)
```
You should see:
```
Type the number you want to play or q to exit:
```
On the second terminal, insert the number in where you want to make your move and hit enter!
The map should update on the top screen and now you can play the game!

View File

@ -21,10 +21,8 @@ defmodule Utils do
"""
@min_print_level 3
@doc """
This function works similiary with unicast but it allows both for a pid or an atom
to be given as the first parameter
"""
# This function works similiary with unicast but it allows both for a pid or an atom
# to be given as the first parameter
def safecast(p, m) when p == nil, do: IO.puts('Trying to safecast #{m} with p as nil')
def safecast(p, m) when is_pid(p), do: send(p, m)
def safecast(p, m) do
@ -132,9 +130,7 @@ defmodule Paxos do
create_log 0
@doc """
This macro allows the state.instmap to be updated very easily
"""
# This macro allows the state.instmap to be updated very easily
defmacrop set_instmap(do: expr) do
quote do
var!(map) = var!(state).instmap[var!(inst)]
@ -171,10 +167,8 @@ defmodule Paxos do
run(state)
end
@doc """
Guarantees that a specific state exists for a specific instance
"""
defp has_or_create(state, inst, value \\ nil, pid_to_inform \\ nil, action \\ nil) do
# Guarantees that a specific state exists for a specific instance
defp has_or_create(state, inst, value \\ nil, pid_to_inform \\ nil) do
or_state state.instmap[inst] == nil do
instmap =
Map.put(state.instmap, inst, %{
@ -189,7 +183,6 @@ defmodule Paxos do
accepted_value: nil,
pid_to_inform: pid_to_inform,
has_sent_accept: false,
action: action,
has_sent_prepare: false,
})
@ -197,11 +190,9 @@ defmodule Paxos do
end
end
@doc """
Checks if an instance has finished or if it was aborted.
If the optional parameter ignore_aborted was set to true makes this function only check
if the the instance has finished
"""
# Checks if an instance has finished or if it was aborted.
# If the optional parameter ignore_aborted was set to true makes this function only check
# if the the instance has finished
defp has_finished(state, inst, ignore_aborted \\ false) do
cond do
Map.has_key?(state.decided, inst) -> true
@ -211,21 +202,19 @@ defmodule Paxos do
end
end
@doc """
This is the run/recieve function
All the messages that are handled by this function are:
{:ele_trust, proc} ->
{:propose, inst, value, pid_to_inform, action} ->
{:rb_deliver, proc, {:other_propose, inst, value}} ->
{:rb_deliver, proc, {:prepare, proc, inst, ballot}} ->
{:nack, inst, ballot} ->
{:rb_deliver, _proc, {:abort, inst, ballot}} ->
{:prepared, inst, ballot, accepted_ballot, accepted_value} ->
{:rb_deliver, proc, {:accept, inst, ballot, value}} ->
{:accepted, inst, ballot} ->
{:get_value, inst, pid_to_inform} ->
{:rb_deliver, _, {:decide, inst, value}} ->
"""
# This is the run/recieve function
# All the messages that are handled by this function are:
# {:ele_trust, proc} ->
# {:propose, inst, value, pid_to_inform} ->
# {:rb_deliver, proc, {:other_propose, inst, value}} ->
# {:rb_deliver, proc, {:prepare, proc, inst, ballot}} ->
# {:nack, inst, ballot} ->
# {:rb_deliver, _proc, {:abort, inst, ballot}} ->
# {:prepared, inst, ballot, accepted_ballot, accepted_value} ->
# {:rb_deliver, proc, {:accept, inst, ballot, value}} ->
# {:accepted, inst, ballot} ->
# {:get_value, inst, pid_to_inform} ->
# {:rb_deliver, _, {:decide, inst, value}} ->
runfn do
# Handles leader elector
{:ele_trust, proc} ->
@ -235,9 +224,9 @@ defmodule Paxos do
prepare(st, inst)
end)
# Handles a proposal from the parent process
{:propose, inst, value, pid_to_inform, action} ->
log("#{state.name} - Propose #{inspect(inst)} with action #{inspect(action)}")
# Handles a proposal message from the parent process
{:propose, inst, value, pid_to_inform} ->
log("#{state.name} - Propose #{inspect(inst)}")
cond do
has_finished(state, inst, true) ->
@ -245,17 +234,9 @@ defmodule Paxos do
send(pid_to_inform, {:decision, inst, state.decided[inst]})
state
action == :increase_ballot_number ->
log("#{state.name} - Got request to increase ballot number for inst #{inst}")
state = has_or_create(state, inst)
set_instmap do
%{map| ballot: Ballot.inc(map.ballot)}
end
not Map.has_key?(state.instmap, inst) ->
EagerReliableBroadcast.broadcast(state.name, {:other_propose, inst, value})
state = has_or_create(state, inst, value, pid_to_inform, action)
state = has_or_create(state, inst, value, pid_to_inform)
prepare(state, inst)
state.instmap[inst].value == nil ->
@ -265,7 +246,6 @@ defmodule Paxos do
%{ map |
value: value,
pid_to_inform: pid_to_inform,
action: action,
}
end
@ -277,7 +257,7 @@ defmodule Paxos do
end
# Handles the sharing of a proposal value to other processes
{:rb_deliver, proc, {:other_propose, inst, value}} ->
{:rb_deliver, _, {:other_propose, inst, value}} ->
cond do
has_finished(state, inst, true) ->
EagerReliableBroadcast.broadcast(
@ -310,6 +290,7 @@ defmodule Paxos do
set_instmap do
%{ map | ballot: ballot }
end
state
Ballot.compare(ballot, &>/2, state.instmap[inst].ballot) ->
safecast(proc,
@ -320,13 +301,14 @@ defmodule Paxos do
set_instmap do
%{ map | ballot: ballot }
end
state
true ->
safecast(proc, {:nack, inst, ballot})
state
end
# Handles a nack
# Handles a nack message
{:nack, inst, ballot} ->
log("#{state.name} - nack #{inspect(inst)} #{inspect(ballot)}")
@ -349,12 +331,13 @@ defmodule Paxos do
}
end
state
true ->
state
end
# Handles an abort
{:rb_deliver, _proc, {:abort, inst, ballot}} ->
# Handles an abort message
{:rb_deliver, _proc, {:abort, inst, _}} ->
cond do
has_finished(state, inst) ->
state
@ -397,7 +380,7 @@ defmodule Paxos do
state
end
# Handles a accept
# Handles an accept message
{:rb_deliver, proc, {:accept, inst, ballot, value}} ->
cond do
has_finished(state, inst) ->
@ -418,6 +401,7 @@ defmodule Paxos do
accepted_ballot: ballot
}
end
state
else
log("#{state.name} -> #{proc} nack")
safecast(proc, {:nack, inst, ballot})
@ -425,7 +409,7 @@ defmodule Paxos do
end
end
# Handles a accept
# Handles an accept message
# Sends out accept when a decide when a quoeom is met
{:accepted, inst, ballot} ->
log("#{state.name} - accepted #{inspect(inst)} #{inspect(ballot)}")
@ -471,10 +455,8 @@ defmodule Paxos do
end
end
@doc """
Does the logic to decide when to send the prepare messages
Also sets the state nessesary to run the proposal
"""
# Does the logic to decide when to send the prepare messages
# Also sets the state nessesary to run the proposal
defp prepare(state, _) when state.leader != state.name, do: state
defp prepare(state, inst) do
cond do
@ -505,15 +487,14 @@ defmodule Paxos do
has_sent_accept: false
}
end
state
end
end
@doc """
Processes the prepared messages and when a quorum is met the accept messages are send
"""
# Processes the prepared messages and when a quorum is met the accept messages are send
defp prepared(state, _) when state.leader != state.name, do: state
defp prepared(state, inst) do
if length(state.instmap[inst].prepared_values) >= floor(length(state.processes) / 2) + 1 and
or_state length(state.instmap[inst].prepared_values) >= floor(length(state.processes) / 2) + 1 and
not state.instmap[inst].has_sent_accept do
{_, a_val} =
Enum.reduce(state.instmap[inst].prepared_values, {Ballot.init(state.name, 0), nil}, fn {bal, val},
@ -550,24 +531,16 @@ defmodule Paxos do
has_sent_accept: true
}
end
else
state
end
end
@doc """
Processes the accepted messages and when a qurum is met decide on the value
"""
# Processes the accepted messages and when a qurum is met decide on the value
defp accepted(state, _) when state.leader != state.name, do: state
defp accepted(state, inst) do
or_state state.instmap[inst].accepted >= floor(length(state.processes) / 2) + 1 do
value = state.instmap[inst].ballot_value
if state.instmap[inst].action == :kill_before_decision do
log("#{state.name} - Leader has action to die before decision #{inspect({:decide, inst, value})}")
Process.exit(self(), :kill)
end
EagerReliableBroadcast.broadcast(
state.name,
{:decide, inst, value}
@ -592,15 +565,13 @@ defmodule Paxos do
@doc """
Send the propose message to the paxos replica and waits for a response from the correct instance
"""
def propose(pid, inst, value, t, action \\ nil) do
send(pid, {:propose, inst, value, self(), action})
def propose(pid, inst, value, t) do
send(pid, {:propose, inst, value, self()})
propose_loop(inst, t)
end
@doc """
Waits the right message from the paxos replica
"""
# Waits the right message from the paxos replica
defp propose_loop(inst, t) do
receive do
{:timeout, ^inst} -> {:timeout}
@ -622,9 +593,7 @@ defmodule Paxos do
get_decision_loop(inst, t)
end
@doc """
Sends waits for the right message from the paxos replica
"""
# Sends waits for the right message from the paxos replica
defp get_decision_loop(inst, t) do
receive do
{:get_value_res, ^inst, v} ->
@ -661,9 +630,7 @@ defmodule Ballot do
}
end
@doc """
Compare the name of 2 processes and select the lowest one
"""
# Compare the name of 2 processes and select the lowest one
defp lexicographical_compare(a, b) do
cond do
a == b -> 0
@ -672,9 +639,7 @@ defmodule Ballot do
end
end
@doc """
Callculate the difference between w ballots
"""
# Callculate the difference between w ballots
defp diff({name1, number1}, {name2, number2}) do
diff = number1 - number2
if diff == 0 do
@ -701,9 +666,7 @@ defmodule EagerReliableBroadcast do
require Utils
import Utils
@doc """
Removes _br from the name of a process
"""
# Removes _br from the name of a process
defp get_non_rb_name(name) do
String.to_atom(String.replace(Atom.to_string(name), "_rb", ""))
end

View File

@ -121,9 +121,7 @@ defmodule Server do
end
@doc """
Checks if the user can play the move before starting the requesting the user to play
"""
# 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 ->
@ -155,7 +153,7 @@ defmodule Server do
not is_number(move) ->
safecast(pid_to_inform, {:make_move, game_id, :invalid_move})
state
move >= length(game.game_state) ->
move >= length(game.game_state) or move < 0 ->
safecast(pid_to_inform, {:make_move, game_id, :invalid_move})
state
true ->
@ -165,9 +163,7 @@ defmodule Server do
end
end
@doc """
Tries to propose to paxos the game action
"""
# Tries to propose to paxos the game action
defp try_to_play(state, game_id, move, pid_to_inform) do
name = state.name
@ -209,9 +205,7 @@ defmodule Server do
end
end
@doc """
Get the most recent game_state and return it to the player
"""
# 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 ->
@ -240,9 +234,7 @@ defmodule Server do
end
end
@doc """
This generates a new hand based on the current game state
"""
# 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
@ -257,9 +249,7 @@ defmodule Server do
end
end
@doc """
This tries to create a game by sending the create message to paxos
"""
# 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)
@ -281,9 +271,8 @@ defmodule Server do
#
# Utils
#
@doc """
Checks if a game has been finished
"""
# Checks if a game has been finished
defp is_finished(state, game) do
case state.games[game] do
{:finished, _} -> true
@ -291,9 +280,7 @@ defmodule Server do
end
end
@doc """
Gets up to the most recent instance
"""
# 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
@ -302,9 +289,7 @@ defmodule Server do
end
end
@doc """
Sets the modified flag
"""
# 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})}
@ -315,6 +300,7 @@ defmodule Server do
# 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)
@ -327,7 +313,10 @@ defmodule Server do
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)
@ -345,7 +334,7 @@ defmodule Server do
list =
indexed_game_state |>
Enum.map(fn {x, ti} -> if ti == i, do: {{:merged, n + 1}, i}, else: {x, ti} end) |>
Enum.filter(fn {x, ti} -> cond do
Enum.filter(fn {_, ti} -> cond do
b_i == ti -> false
a_i == ti -> false
true -> true
@ -376,7 +365,7 @@ defmodule Server do
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 {x, ti} -> cond do
Enum.filter(fn {_, ti} -> cond do
b_i == ti -> false
a_i == ti -> false
true -> true
@ -388,7 +377,8 @@ defmodule Server do
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
@ -396,6 +386,9 @@ defmodule Server do
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)
@ -412,7 +405,7 @@ defmodule Server do
_ ->
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 {x, ti} -> cond do
Enum.filter(fn {_, ti} -> cond do
b_i == ti -> false
a_i == ti -> false
true -> true
@ -428,7 +421,8 @@ defmodule Server do
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)
@ -438,18 +432,14 @@ defmodule Server do
[list, 0..(length(list) - 1)] |> Enum.zip()
end
defp remove_merged([], rec, _), do: rec
defp remove_merged([{:merged, n} | tl], rec, add),
do: if add, do: remove_merged(tl, rec ++ [n], false),
else: remove_merged(tl, rec, false)
defp remove_merged([n | tl], rec, add), do:
remove_merged(tl, rec ++ [n], add)
defp remove_merged(list), do: remove_merged(list, [], true)
# 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 |>
@ -474,7 +464,8 @@ defmodule Server do
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
@ -507,6 +498,7 @@ defmodule Server do
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] ->
@ -534,18 +526,23 @@ defmodule Server do
############
# 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")
@ -561,11 +558,15 @@ defmodule Server do
{: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")
@ -587,6 +588,9 @@ defmodule Server do
{: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)
@ -595,23 +599,36 @@ defmodule Server do
############
# Debug
############
@doc """
Quicky creates some servers it's useful in testing
"""
def spinup(number_of_participants) do
procs = Enum.to_list(0..number_of_participants) |> Enum.map(fn n -> :"p#{n}" end)
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)
@ -675,6 +692,11 @@ defmodule Client do
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()
@ -707,10 +729,15 @@ defmodule Client do
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)
cont = case game do
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")
@ -732,44 +759,50 @@ defmodule Client do
end
end
end
def to_name(list) when is_list(list), do: list |> Enum.map(fn x -> to_name(x) end)
def to_name(atom) when atom == :+, do: IO.ANSI.color_background(9) <> IO.ANSI.color(15) <> " + " <> IO.ANSI.reset()
def to_name(atom) when atom == :-, do: IO.ANSI.color_background(21) <> IO.ANSI.color(15) <> " - " <> IO.ANSI.reset()
def to_name(atom) when atom == :b, do: IO.ANSI.color_background(232) <> IO.ANSI.color(15) <> " b " <> IO.ANSI.reset()
def to_name(atom) when is_atom(atom), do: atom
def to_name(i) do
# 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
def interpolate(list) do
# 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
def grow_empty_list(t, i, acc) when i == 0, do: t ++ acc
def grow_empty_list([], i, acc), do: grow_empty_list(acc, i, [])
def grow_empty_list([h | tl], i, acc), do:
# 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 ++ [" "] ])
def fill(list) do
# 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
def fill([], _, acc), do: acc
def fill([hd | tail], [add_hd | add_tail], acc) do
defp fill([], _, acc), do: acc
defp fill([hd | tail], [add_hd | add_tail], acc) do
fill(tail, add_tail ,acc ++ [hd] ++ add_hd)
end
def printpt(game_state, hand), do: printpt(game_state, hand, 0)
def printpt(_, _, i) when i > 16, do: nil
def printpt(game_state, hand, i) do
# 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" |>

View File

@ -66,25 +66,25 @@ test_suite = [
# Aditional Test functions
{&PaxosTestAditional.run_leader_crash_simple_before_decision/3, TestUtil.get_dist_config(host, 5), 10,
"Leader crashes right before decision, no concurrent ballots, 5 nodes"},
{&PaxosTestAditional.run_leader_crash_simple_before_decision/3, TestUtil.get_local_config(5), 10,
"Leader crashes right before decision, no concurrent ballots, 5 local procs"},
{&PaxosTestAditional.run_non_leader_send_propose_after_leader_elected/3, TestUtil.get_dist_config(host, 5), 10,
"Non-Leader proposes after leader is elected, 5 nodes"},
{&PaxosTestAditional.run_non_leader_send_propose_after_leader_elected/3, TestUtil.get_local_config(5), 10,
"Non-Leader proposes after leader is elected, 5 local procs"},
{&PaxosTestAditional.run_leader_should_nack_simple/3, TestUtil.get_dist_config(host, 5), 10,
"Leader should nack before decision and then come to decision, no concurrent ballots, 5 nodes"},
{&PaxosTestAditional.run_leader_should_nack_simple/3, TestUtil.get_local_config(5), 10,
"Leader should nack before decision and then come to decision, 5 local procs"},
{&PaxosTestAditional.run_non_leader_should_nack_simple/3, TestUtil.get_dist_config(host, 5), 10,
"Non-Leader should nack before decision and then come to decision, no concurrent ballots, 5 nodes"},
{&PaxosTestAditional.run_non_leader_should_nack_simple/3, TestUtil.get_local_config(5), 10,
"Non-Leader should nack before decision and then come to decision, 5 local procs"},
# {&PaxosTestAditional.run_leader_crash_simple_before_decision/3, TestUtil.get_dist_config(host, 5), 10,
# "Leader crashes right before decision, no concurrent ballots, 5 nodes"},
# {&PaxosTestAditional.run_leader_crash_simple_before_decision/3, TestUtil.get_local_config(5), 10,
# "Leader crashes right before decision, no concurrent ballots, 5 local procs"},
#
# {&PaxosTestAditional.run_non_leader_send_propose_after_leader_elected/3, TestUtil.get_dist_config(host, 5), 10,
# "Non-Leader proposes after leader is elected, 5 nodes"},
# {&PaxosTestAditional.run_non_leader_send_propose_after_leader_elected/3, TestUtil.get_local_config(5), 10,
# "Non-Leader proposes after leader is elected, 5 local procs"},
#
# {&PaxosTestAditional.run_leader_should_nack_simple/3, TestUtil.get_dist_config(host, 5), 10,
# "Leader should nack before decision and then come to decision, no concurrent ballots, 5 nodes"},
# {&PaxosTestAditional.run_leader_should_nack_simple/3, TestUtil.get_local_config(5), 10,
# "Leader should nack before decision and then come to decision, 5 local procs"},
#
# {&PaxosTestAditional.run_non_leader_should_nack_simple/3, TestUtil.get_dist_config(host, 5), 10,
# "Non-Leader should nack before decision and then come to decision, no concurrent ballots, 5 nodes"},
# {&PaxosTestAditional.run_non_leader_should_nack_simple/3, TestUtil.get_local_config(5), 10,
# "Non-Leader should nack before decision and then come to decision, 5 local procs"},