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

View File

@ -121,9 +121,7 @@ defmodule Server do
end 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 defp try_to_play_checks(state, game_id, move, pid_to_inform, repeat \\ false) do
cond do cond do
state.games[game_id] == :not_playing_in_game -> state.games[game_id] == :not_playing_in_game ->
@ -155,7 +153,7 @@ defmodule Server do
not is_number(move) -> not is_number(move) ->
safecast(pid_to_inform, {:make_move, game_id, :invalid_move}) safecast(pid_to_inform, {:make_move, game_id, :invalid_move})
state state
move >= length(game.game_state) -> move >= length(game.game_state) or move < 0 ->
safecast(pid_to_inform, {:make_move, game_id, :invalid_move}) safecast(pid_to_inform, {:make_move, game_id, :invalid_move})
state state
true -> true ->
@ -165,9 +163,7 @@ defmodule Server do
end end
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 defp try_to_play(state, game_id, move, pid_to_inform) do
name = state.name name = state.name
@ -209,9 +205,7 @@ defmodule Server do
end end
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 defp get_game_state(state, game_id, pid_to_inform, repeat \\ false) do
cond do cond do
state.games[game_id] == :not_playing_in_game -> state.games[game_id] == :not_playing_in_game ->
@ -240,9 +234,7 @@ defmodule Server do
end end
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 defp get_hand_for_game_state(game_state) do
r1 = Enum.random(0..100) r1 = Enum.random(0..100)
cond do cond do
@ -257,9 +249,7 @@ defmodule Server do
end end
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 defp try_to_create_game(state, participants) do
game_ids = Map.keys(state.games) game_ids = Map.keys(state.games)
latest = Enum.at(Enum.sort(game_ids), length(game_ids) - 1) latest = Enum.at(Enum.sort(game_ids), length(game_ids) - 1)
@ -281,9 +271,8 @@ defmodule Server do
# #
# Utils # Utils
# #
@doc """
Checks if a game has been finished # Checks if a game has been finished
"""
defp is_finished(state, game) do defp is_finished(state, game) do
case state.games[game] do case state.games[game] do
{:finished, _} -> true {:finished, _} -> true
@ -291,9 +280,7 @@ defmodule Server do
end end
end end
@doc """ # Gets up to the most recent instance
Gets up to the most recent instance
"""
defp qurey_status(state) do defp qurey_status(state) do
v = Paxos.get_decision(state.paxos, state.instance, 100) v = Paxos.get_decision(state.paxos, state.instance, 100)
or_state v != nil do or_state v != nil do
@ -302,9 +289,7 @@ defmodule Server do
end end
end end
@doc """ # Sets the modified flag
Sets the modified flag
"""
defp set_modifed(state, game, val \\ false) do 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 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})} %{state | games: Map.put(state.games, game, %{state.games[game] | modified: val})}
@ -315,6 +300,7 @@ defmodule Server do
# Apply Game States # Apply Game States
# #
# Calculates the index based on the incoming index and a move
defp get_index(indexed_game_state, spos, index) do defp get_index(indexed_game_state, spos, index) do
index = spos + index index = spos + index
len = length(indexed_game_state) len = length(indexed_game_state)
@ -327,7 +313,10 @@ defmodule Server do
index index
end end
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([], indexed_game_state), do: {false, indexed_game_state}
defp simplify_game_state_pluses([{:+, i} | tl], indexed_game_state) do defp simplify_game_state_pluses([{:+, i} | tl], indexed_game_state) do
before_i = get_index(indexed_game_state, i, -1) before_i = get_index(indexed_game_state, i, -1)
@ -345,7 +334,7 @@ defmodule Server do
list = list =
indexed_game_state |> indexed_game_state |>
Enum.map(fn {x, ti} -> if ti == i, do: {{:merged, n + 1}, i}, else: {x, ti} end) |> 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 b_i == ti -> false
a_i == ti -> false a_i == ti -> false
true -> true true -> true
@ -376,7 +365,7 @@ defmodule Server do
list = list =
indexed_game_state |> 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.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 b_i == ti -> false
a_i == ti -> false a_i == ti -> false
true -> true true -> true
@ -388,7 +377,8 @@ defmodule Server do
simplify_game_state_pluses(tl, indexed_game_state) simplify_game_state_pluses(tl, indexed_game_state)
end end
end end
# Check if the item in an game is the substate merged
defp is_merged(item) do defp is_merged(item) do
case item do case item do
{:merged, _} -> true {:merged, _} -> true
@ -396,6 +386,9 @@ defmodule Server do
end end
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 defp expand_merge(indexed_game_state) do
{{:merged, n}, i} = indexed_game_state |> Enum.find(fn {x, _} -> is_merged(x) end) {{:merged, n}, i} = indexed_game_state |> Enum.find(fn {x, _} -> is_merged(x) end)
@ -412,7 +405,7 @@ defmodule Server do
_ -> _ ->
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.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 b_i == ti -> false
a_i == ti -> false a_i == ti -> false
true -> true true -> true
@ -428,7 +421,8 @@ defmodule Server do
indexed_game_state indexed_game_state
end end
end end
# This funcion removes previous indexes and reindexs the state
defp reindex(list, flat \\ true) do defp reindex(list, flat \\ true) do
list = if flat do list = if flat do
list |> Enum.map(fn {n, _} -> n end) list |> Enum.map(fn {n, _} -> n end)
@ -438,18 +432,14 @@ defmodule Server do
[list, 0..(length(list) - 1)] |> Enum.zip() [list, 0..(length(list) - 1)] |> Enum.zip()
end end
defp remove_merged([], rec, _), do: rec # Removes all merged from the array
defp remove_merged(l), do: l |> Enum.map(fn x -> case x do
defp remove_merged([{:merged, n} | tl], rec, add), {:merged, n} -> n
do: if add, do: remove_merged(tl, rec ++ [n], false), x -> x
else: remove_merged(tl, rec, false) end end)
defp remove_merged([n | tl], rec, add), do: # This funcion recieves the game state after the move was done and tries to simplify the game
remove_merged(tl, rec ++ [n], add)
defp remove_merged(list), do: remove_merged(list, [], true)
defp simplify_game_state(game_state) do defp simplify_game_state(game_state) do
indexed_game_state = indexed_game_state =
game_state |> game_state |>
@ -474,7 +464,8 @@ defmodule Server do
indexed_game_state |> Enum.map(fn {v, _} -> v end) indexed_game_state |> Enum.map(fn {v, _} -> v end)
end 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 defp apply_game(state, {:make_move, game_id, player_name, pos_move, new_hand}) do
game = state.games[game_id] game = state.games[game_id]
case game do case game do
@ -507,6 +498,7 @@ defmodule Server do
end 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 defp apply_game(state, {:start_game, game_id, participants, new_game_state, hand}) do
cond do cond do
state.games[game_id] -> state.games[game_id] ->
@ -534,18 +526,23 @@ defmodule Server do
############ ############
# Interface # Interface
############ ############
# handles responses from the start game request
create_loop :start_game do create_loop :start_game do
{:start_game_ans, game_id} -> {:start_game_ans, game_id} ->
log("Started a game #{game_id}") log("Started a game #{game_id}")
{:start_game, game_id} {:start_game, game_id}
end end
@doc """
Requests the server to start a game
"""
def start_game(name, participants) do def start_game(name, participants) do
safecast(name, {:start_game, participants, self()}) safecast(name, {:start_game, participants, self()})
start_game_loop(nil, 10000) start_game_loop(nil, 10000)
end end
# handles responses from the get game state request
create_loop :get_game_state do create_loop :get_game_state do
{:game_state, ^v, :not_playing} -> {:game_state, ^v, :not_playing} ->
log("Not Playing in that game") log("Not Playing in that game")
@ -561,11 +558,15 @@ defmodule Server do
{:not_exists} {:not_exists}
end end
@doc """
Requests the server to get the game state
"""
def get_game_state(name, game_id) do def get_game_state(name, game_id) do
safecast(name, {:get_game_state, game_id, self()}) safecast(name, {:get_game_state, game_id, self()})
get_game_state_loop(game_id, 10000) get_game_state_loop(game_id, 10000)
end end
# handles responses from the make move request
create_loop :make_move do create_loop :make_move do
{:make_move, ^v, :game_does_not_exist} -> {:make_move, ^v, :game_does_not_exist} ->
log("Got game does not exist") log("Got game does not exist")
@ -587,6 +588,9 @@ defmodule Server do
{:state, game_state, hand} {:state, game_state, hand}
end end
@doc """
Requests to make a move
"""
def make_move(name, game_id, move) do def make_move(name, game_id, move) do
safecast(name, {:make_move, game_id, move, self()}) safecast(name, {:make_move, game_id, move, self()})
make_move_loop(game_id, 10000) make_move_loop(game_id, 10000)
@ -595,23 +599,36 @@ defmodule Server do
############ ############
# Debug # Debug
############ ############
@doc """
Quicky creates some servers it's useful in testing
"""
def spinup(number_of_participants) do 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) Enum.map(procs, fn proc -> Server.start(proc, procs) end)
end end
@doc """
Quicky kills some servers it's useful in testing
"""
def kill (pids) do def kill (pids) do
pids |> Enum.map(fn m -> Process.exit(m, :kill) end) pids |> Enum.map(fn m -> Process.exit(m, :kill) end)
end end
end end
defmodule Client do defmodule Client do
@moduledoc """
This module handles displaying the game state in a nice to understand
way
"""
import Utils import Utils
require Utils require Utils
create_log 3 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 def play_game(process, game_id) do
game = Server.get_game_state(process, game_id) game = Server.get_game_state(process, game_id)
@ -675,6 +692,11 @@ defmodule Client do
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 def control_game(process, game_id) do
to_play = IO.gets("Type the number you want to play or q to exit: ") to_play = IO.gets("Type the number you want to play or q to exit: ")
to_play = to_play |> String.trim("\"") |> String.trim() to_play = to_play |> String.trim("\"") |> String.trim()
@ -707,10 +729,15 @@ defmodule Client do
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 def display_game(process, game_id, temp_game \\ [], temp_hand \\ []) do
game = Server.get_game_state(process, game_id) game = Server.get_game_state(process, game_id)
cont = case game do case game do
:timeout -> log("Could to not comunicate with the server") :timeout -> log("Could to not comunicate with the server")
{:not_exists} -> log("Game does not exist") {:not_exists} -> log("Game does not exist")
{:not_playing} -> log("Not Playing in that game") {:not_playing} -> log("Not Playing in that game")
@ -732,44 +759,50 @@ defmodule Client do
end end
end end
end end
def to_name(list) when is_list(list), do: list |> Enum.map(fn x -> to_name(x) end) # This funcion recieves some objects and transforms them into user friendly text
def to_name(atom) when atom == :+, do: IO.ANSI.color_background(9) <> IO.ANSI.color(15) <> " + " <> IO.ANSI.reset() defp 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(21) <> IO.ANSI.color(15) <> " - " <> IO.ANSI.reset() defp to_name(atom) when atom == :+, do: IO.ANSI.color_background(9) <> 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() defp to_name(atom) when atom == :-, do: IO.ANSI.color_background(21) <> IO.ANSI.color(15) <> " - " <> IO.ANSI.reset()
def to_name(atom) when is_atom(atom), do: atom defp to_name(atom) when atom == :b, do: IO.ANSI.color_background(232) <> IO.ANSI.color(15) <> " b " <> IO.ANSI.reset()
def to_name(i) do 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) 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, 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() 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() color <> String.pad_leading("#{letter}", 3, " ") <> IO.ANSI.reset()
end end
def interpolate(list) do # Adds the index as an indicator and pads the index
defp interpolate(list) do
[list, 0..length(list) - 1] |> [list, 0..length(list) - 1] |>
Enum.zip() |> Enum.zip() |>
Enum.reduce([], fn {v, i}, acc -> acc ++ [String.pad_leading("#{i}", 3, " "), v] end) Enum.reduce([], fn {v, i}, acc -> acc ++ [String.pad_leading("#{i}", 3, " "), v] end)
end end
def grow_empty_list(t, i, acc) when i == 0, do: t ++ acc # This grows a list with between spaces every item of the interpolated list
def grow_empty_list([], i, acc), do: grow_empty_list(acc, i, []) # This allows the items to take as puch space in the possilble
def grow_empty_list([h | tl], i, acc), do: 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 ++ [" "] ]) 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_fill = 32 - length(list)
to_add = grow_empty_list(Enum.map(list, fn _ -> [] end), to_fill, []) to_add = grow_empty_list(Enum.map(list, fn _ -> [] end), to_fill, [])
fill(list, to_add, []) fill(list, to_add, [])
end end
def fill([], _, acc), do: acc defp fill([], _, acc), do: acc
def fill([hd | tail], [add_hd | add_tail], acc) do defp fill([hd | tail], [add_hd | add_tail], acc) do
fill(tail, add_tail ,acc ++ [hd] ++ add_hd) fill(tail, add_tail ,acc ++ [hd] ++ add_hd)
end end
def printpt(game_state, hand), do: printpt(game_state, hand, 0) # This functions prints the circle
def printpt(_, _, i) when i > 16, do: nil defp printpt(game_state, hand), do: printpt(game_state, hand, 0)
def printpt(game_state, hand, i) do defp printpt(_, _, i) when i > 16, do: nil
defp printpt(game_state, hand, i) do
res = case i do res = case i do
0 -> 0 ->
" xxx \n" |> " xxx \n" |>

View File

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