Compare commits

...

24 Commits

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
0a9f086d5d fixed ci
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-19 18:50:22 +00:00
b337101566 fixed ci
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is failing
2024-01-19 18:46:24 +00:00
c33b7edef6 added ci to generate submission files
Some checks failed
continuous-integration/drone/push Build is failing
2024-01-19 18:45:43 +00:00
8c674626f8 added some docs
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-19 18:23:08 +00:00
a22005c868 added some comments
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-19 14:32:55 +00:00
cc7dc7cea8 disabled logs
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-18 23:13:58 +00:00
b4d0527b41 Fixed the funk
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-18 23:13:34 +00:00
b015d8cabd fixed spaming the server
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-18 22:53:12 +00:00
ee9f5e1187 Fixed the funk
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-18 22:28:10 +00:00
1756972e8c Made game interactive 2024-01-18 19:40:04 +00:00
6 changed files with 1102 additions and 296 deletions

View File

@@ -10,6 +10,18 @@ steps:
- marp presentation.md -o index.html - marp presentation.md -o index.html
- cp * /home/andr3/services/notes/presentation - cp * /home/andr3/services/notes/presentation
- name: Create release
environment:
TOKEN:
from_secret: token
commands:
- cp lib/server.ex .
- zip server.zip server.ex README.md
- tea login add --url https://git.andr3h3nriqu3s.com --token "$TOKEN"
- tea r rm -y latest || echo "Release not found"
- tea r c --title "Latest" --asset "README.md" --asset "lib/paxos.ex" --asset "lib/server.ex" --asset "server.zip" latest
trigger: trigger:
branch: branch:
- main - main

245
README.md Normal file
View File

@@ -0,0 +1,245 @@
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

@@ -1,7 +1,28 @@
defmodule Utils do #
# This files contains some modules nessesary for paxos to work
#
# Utils: General Utilities
# Paxos: Paxos logic
# Ballot: Contain the logic to handle the ballot numbers
# EventualLeaderElector: Leader elector code
# EagerReliableBroadcast: Handles eager reliable broadcast
#
@min_print_level 1 defmodule Utils do
@moduledoc """
This module contains some helpful functions are used throughout all modules
"""
@doc """
Sets the min log level
0 - show all
1 - ignore paxos
2 - ignore server
"""
@min_print_level 3
# 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 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
@@ -11,9 +32,19 @@ defmodule Utils do
end end
end end
@doc """
This function creates a new name for processes
"""
def alter_name(name, part), do: :"#{name}#{part}" def alter_name(name, part), do: :"#{name}#{part}"
@doc """
This function sends a message to a list of porcesses
"""
def beb_broadcast(m, dest), do: for(p <- dest, do: safecast(p, m)) def beb_broadcast(m, dest), do: for(p <- dest, do: safecast(p, m))
@doc """
This function register a pid in the global name space
"""
def register_name(name, pid, link \\ true) do def register_name(name, pid, link \\ true) do
case :global.re_register_name(name, pid) do case :global.re_register_name(name, pid) do
:yes -> :yes ->
@@ -29,7 +60,10 @@ defmodule Utils do
:error :error
end end
end end
@doc """
This macro defines a new log function for a specific log level
"""
defmacro create_log(level) do defmacro create_log(level) do
quote do quote do
def log(msg) do def log(msg) do
@@ -37,13 +71,26 @@ defmodule Utils do
end end
end end
end end
@doc """
This function is used bu the create_log macro.
This function only prints logs if the level definied in the create_log macro
"""
def _log(msg, level) do def _log(msg, level) do
if (@min_print_level <= level) do if (@min_print_level <= level) do
IO.puts(msg) IO.puts(msg)
end end
end end
@doc """
This macro defines a an or_state function that acts like this:
if val do
expr
else
state
end
"""
defmacro or_state(val, do: expr) do defmacro or_state(val, do: expr) do
quote do quote do
case unquote(val) do case unquote(val) do
@@ -52,7 +99,10 @@ defmodule Utils do
end end
end end
end end
@doc """
This macro defines a macro to simplify the process of creating run functions
"""
defmacro runfn(do: expr) do defmacro runfn(do: expr) do
quote do quote do
def run(s) do def run(s) do
@@ -65,37 +115,43 @@ defmodule Utils do
end end
end end
#
#
# Possible actions
# :kill_before_decision
# :increase_ballot_number - this makes it so that it does not propose but jump simply increases the number of the current ballot
# this is usefull when forcing a nack
#
defmodule Paxos do defmodule Paxos do
@moduledoc """
This module contains all the logic for Paxos
To start a paxos instance run Paxos.start
To propose a a value to paxos run Paxos.propose
To get a previous decision please run Paxos.get_decision
"""
require Utils require Utils
import Utils import Utils
create_log 0 create_log 0
defmacro set_instmap(do: expr) do # This macro allows the state.instmap to be updated very easily
defmacrop set_instmap(do: expr) do
quote do quote do
var!(map) = var!(state).instmap[var!(inst)] var!(map) = var!(state).instmap[var!(inst)]
new_instmap = Map.put(var!(state).instmap, var!(inst), unquote(expr)) new_instmap = Map.put(var!(state).instmap, var!(inst), unquote(expr))
var!(state) = %{var!(state) | instmap: new_instmap } var!(state) = %{var!(state) | instmap: new_instmap }
end end
end end
# Starts the Paxos replica with a specific name and some processes @doc """
Starts the Paxos replica with a specific name and some processes
"""
def start(name, processes, link \\ false) do def start(name, processes, link \\ false) do
log("Starting paxos for #{name}") log("Starting paxos for #{name}")
pid = spawn(Paxos, :init, [name, processes]) pid = spawn(Paxos, :init, [name, processes])
register_name(name, pid, link) register_name(name, pid, link)
end end
# Init event must be the first @doc """
# one after the component is created Inicializes the state starts the eager reliable broadcast, and starts the eventual leader eletector
"""
def init(name, processes) do def init(name, processes) do
EventualLeaderElector.start(name, processes) EventualLeaderElector.start(name, processes)
EagerReliableBroadcast.start(name, processes) EagerReliableBroadcast.start(name, processes)
@@ -110,9 +166,9 @@ defmodule Paxos do
run(state) run(state)
end end
# Guarantees that a specific state exists # Guarantees that a specific state exists for a specific instance
def has_or_create(state, inst, value \\ nil, pid_to_inform \\ nil, action \\ nil) do defp has_or_create(state, inst, value \\ nil, pid_to_inform \\ 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, %{
@@ -127,15 +183,17 @@ 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,
}) })
%{state | instmap: instmap} %{state | instmap: instmap}
end end
end end
def has_finished(state, inst, ignore_aborted \\ false) do # 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 cond do
Map.has_key?(state.decided, inst) -> true Map.has_key?(state.decided, inst) -> true
ignore_aborted -> false ignore_aborted -> false
@@ -143,17 +201,32 @@ defmodule Paxos do
true -> false true -> false
end end
end end
# 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 runfn do
# Handles leader elector
{:ele_trust, proc} -> {:ele_trust, proc} ->
log("#{state.name} - #{proc} is leader") log("#{state.name} - #{proc} is leader")
Enum.reduce(Map.keys(state.instmap), %{state | leader: proc}, fn inst, st -> Enum.reduce(Map.keys(state.instmap), %{state | leader: proc}, fn inst, st ->
prepare(st, inst) prepare(st, inst)
end) end)
{:propose, inst, value, pid_to_inform, action} -> # Handles a proposal message from the parent process
log("#{state.name} - Propose #{inspect(inst)} with action #{inspect(action)}") {:propose, inst, value, pid_to_inform} ->
log("#{state.name} - Propose #{inspect(inst)}")
cond do cond do
has_finished(state, inst, true) -> has_finished(state, inst, true) ->
@@ -161,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 ->
@@ -181,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
@@ -191,8 +255,9 @@ defmodule Paxos do
EagerReliableBroadcast.broadcast(state.name, {:other_propose, inst, value}) EagerReliableBroadcast.broadcast(state.name, {:other_propose, inst, value})
prepare(state, inst) prepare(state, inst)
end end
{:rb_deliver, proc, {:other_propose, inst, value}} -> # Handles the sharing of a proposal value to other processes
{:rb_deliver, _, {:other_propose, inst, value}} ->
cond do cond do
has_finished(state, inst, true) -> has_finished(state, inst, true) ->
EagerReliableBroadcast.broadcast( EagerReliableBroadcast.broadcast(
@@ -208,7 +273,8 @@ defmodule Paxos do
end end
prepare(state, inst) prepare(state, inst)
end end
# Handles a prepare request from the leader
{:rb_deliver, proc, {:prepare, proc, inst, ballot}} -> {:rb_deliver, proc, {:prepare, proc, inst, ballot}} ->
log("#{state.name} - prepare from #{proc}") log("#{state.name} - prepare from #{proc}")
@@ -224,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,
@@ -234,12 +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 message
{:nack, inst, ballot} -> {:nack, inst, ballot} ->
log("#{state.name} - nack #{inspect(inst)} #{inspect(ballot)}") log("#{state.name} - nack #{inspect(inst)} #{inspect(ballot)}")
@@ -262,11 +331,13 @@ defmodule Paxos do
} }
end end
state
true -> true ->
state state
end end
{:rb_deliver, _proc, {:abort, inst, ballot}} -> # Handles an abort message
{:rb_deliver, _proc, {:abort, inst, _}} ->
cond do cond do
has_finished(state, inst) -> has_finished(state, inst) ->
state state
@@ -281,6 +352,8 @@ defmodule Paxos do
state state
end end
# Handles a prepared
# Sends out accept when a quorum is met
{:prepared, inst, ballot, accepted_ballot, accepted_value} -> {:prepared, inst, ballot, accepted_ballot, accepted_value} ->
log( log(
"#{state.name} - prepared #{inspect(inst)} #{inspect(ballot)} #{inspect(accepted_ballot)} #{inspect(accepted_value)}" "#{state.name} - prepared #{inspect(inst)} #{inspect(ballot)} #{inspect(accepted_ballot)} #{inspect(accepted_value)}"
@@ -307,6 +380,7 @@ defmodule Paxos do
state state
end end
# 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) ->
@@ -327,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})
@@ -334,6 +409,8 @@ defmodule Paxos do
end end
end end
# Handles an accept message
# 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)}")
@@ -351,14 +428,17 @@ defmodule Paxos do
true -> true ->
state state
end end
# handles a parent request to get a value
{:get_value, inst, pid_to_inform} -> {:get_value, inst, pid_to_inform} ->
# log("#{state.name} get_value") # log("#{state.name} get_value")
if has_finished(state, inst, true) do if has_finished(state, inst, true) do
safecast(pid_to_inform, {:get_value_res, inst, state.decided[inst]}) safecast(pid_to_inform, {:get_value_res, inst, state.decided[inst]})
end end
state state
# Adds a decision made by a leader to the list of decisions
# and informs the parent of the decision
{:rb_deliver, _, {:decide, inst, value}} -> {:rb_deliver, _, {:decide, inst, value}} ->
log("#{state.name} - decided #{inspect(inst)} #{inspect(value)}") log("#{state.name} - decided #{inspect(inst)} #{inspect(value)}")
@@ -375,13 +455,10 @@ defmodule Paxos do
end end
end end
# # Does the logic to decide when to send the prepare messages
# Puts process in the preapre state # Also sets the state nessesary to run the proposal
# defp prepare(state, _) when state.leader != state.name, do: state
defp prepare(state, inst) do
def prepare(state, _) when state.leader != state.name, do: state
def prepare(state, inst) do
cond do cond do
state.instmap[inst] == nil -> state.instmap[inst] == nil ->
state state
@@ -410,16 +487,14 @@ defmodule Paxos do
has_sent_accept: false has_sent_accept: false
} }
end end
state
end end
end end
# # Processes the prepared messages and when a quorum is met the accept messages are send
# Process the prepared responses defp prepared(state, _) when state.leader != state.name, do: state
# defp prepared(state, inst) do
def prepared(state, _) when state.leader != state.name, do: state or_state length(state.instmap[inst].prepared_values) >= floor(length(state.processes) / 2) + 1 and
def prepared(state, inst) do
if 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},
@@ -456,25 +531,16 @@ defmodule Paxos do
has_sent_accept: true has_sent_accept: true
} }
end end
else
state state
end end
end end
# # Processes the accepted messages and when a qurum is met decide on the value
# Process the accepted responses defp accepted(state, _) when state.leader != state.name, do: state
# defp accepted(state, inst) do
def accepted(state, _) when state.leader != state.name, do: state or_state state.instmap[inst].accepted >= floor(length(state.processes) / 2) + 1 do
def accepted(state, inst) do
if 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}
@@ -489,69 +555,72 @@ defmodule Paxos do
| decided: Map.put(state.decided, inst, value), | decided: Map.put(state.decided, inst, value),
instmap: Map.delete(state.instmap, inst) instmap: Map.delete(state.instmap, inst)
} }
else
state
end end
end end
def propose(pid, inst, value, t, action \\ nil) do #######################
send(pid, {:propose, inst, value, self(), action}) # Interface #
#######################
@doc """
Send the propose message to the paxos replica and waits for a response from the correct instance
"""
def propose(pid, inst, value, t) do
send(pid, {:propose, inst, value, self()})
propose_loop({inst, t}) propose_loop(inst, t)
end end
def propose_loop(input) do # Waits the right message from the paxos replica
{_, t} = input defp propose_loop(inst, t) do
receive do receive do
{:timeout, inst} -> {:timeout, ^inst} -> {:timeout}
check_and_apply({:timeout}, inst, input, &propose_loop/1) {:abort, ^inst} -> {:abort}
{:decision, ^inst, d} -> {:decision, d}
{:abort, inst} ->
check_and_apply({:abort}, inst, input, &propose_loop/1)
{:decision, inst, d} ->
check_and_apply({:decision, d}, inst, input, &propose_loop/1)
x -> x ->
Process.send_after(self(), x, 500) Process.send_after(self(), x, 500)
propose_loop(input) propose_loop(inst, t)
after after
t -> {:timeout} t -> {:timeout}
end end
end end
@doc """
Sends the :get_value message to the paxos replica
"""
def get_decision(pid, inst, t) do def get_decision(pid, inst, t) do
send(pid, {:get_value, inst, self()}) send(pid, {:get_value, inst, self()})
get_decision_loop({inst, t}) get_decision_loop(inst, t)
end end
def get_decision_loop(input) do # Sends waits for the right message from the paxos replica
{_, t} = input defp get_decision_loop(inst, t) do
receive do receive do
{:get_value_res, inst, v} -> {:get_value_res, ^inst, v} ->
check_and_apply(v, inst, input, &get_decision_loop/1) v
x -> x ->
Process.send_after(self(), x, 500) Process.send_after(self(), x, 500)
get_decision_loop(input) get_decision_loop(inst, t)
after after
t -> nil t -> nil
end end
end end
def check_and_apply(v, inst, input, fun) do
{inInst, _} = input
if inst == inInst do
v
else
fun.(input)
end
end
end end
defmodule Ballot do defmodule Ballot do
def init(name, number \\ 0), do: {name, number} @moduledoc """
A set of a helper functions to manage ballots
"""
@doc """
Create a new ballot
"""
def init(name, number \\ 0), do: {name, number}
@doc """
Increase the ballot number
"""
def inc(b, name \\ nil) do def inc(b, name \\ nil) do
{old_name, number} = b {old_name, number} = b
@@ -560,7 +629,8 @@ defmodule Ballot do
number + 1 number + 1
} }
end end
# 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
@@ -569,7 +639,7 @@ defmodule Ballot do
end end
end end
# 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
@@ -578,25 +648,40 @@ defmodule Ballot do
diff diff
end end
end end
@doc """
Compare 2 ballots
"""
def compare(b1, operator, b2), do: operator.(diff(b1, b2), 0) def compare(b1, operator, b2), do: operator.(diff(b1, b2), 0)
end end
defmodule EagerReliableBroadcast do defmodule EagerReliableBroadcast do
@moduledoc """
This modules implents eager reiable broadcast with the optimization of removing the from the deliver list after hearing
the same number of echos as there is processes
emits {:rb_deliver, proc, message}
"""
require Utils require Utils
import Utils import Utils
def get_non_rb_name(name) do # 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", "")) String.to_atom(String.replace(Atom.to_string(name), "_rb", ""))
end end
@doc """
Starts the Eager reliable process and registers it with a name and links the process with the parent process
"""
def start(name, processes) do def start(name, processes) do
pid = spawn(EagerReliableBroadcast, :init, [name, processes]) pid = spawn(EagerReliableBroadcast, :init, [name, processes])
register_name(alter_name(name, "_rb"), pid) register_name(alter_name(name, "_rb"), pid)
end end
# Init event must be the first @doc """
# one after the component is created Sets state and calls the run function
"""
def init(parent, processes) do def init(parent, processes) do
state = %{ state = %{
name: alter_name(parent, "_rb"), name: alter_name(parent, "_rb"),
@@ -639,17 +724,24 @@ defmodule EagerReliableBroadcast do
############# #############
# Interface # # Interface #
############# #############
@doc """
Sends a a broadcast request to the eager reliable broadcast replica
"""
def broadcast(name, m), do: safecast(alter_name(name, "_rb"), {:broadcast, m}) def broadcast(name, m), do: safecast(alter_name(name, "_rb"), {:broadcast, m})
end end
#
# Emits {:ele_trust, proc }
#
defmodule EventualLeaderElector do defmodule EventualLeaderElector do
@moduledoc """
This modules implents eventual leader elector
emits {:ele_leader, proc}
"""
require Utils require Utils
import Utils import Utils
@doc """
Initializes the leader elector process and registers is under name_ele and links it with the parent process
"""
def start(name, processes) do def start(name, processes) do
new_name = alter_name(name, "_ele") new_name = alter_name(name, "_ele")
pid = spawn(EventualLeaderElector, :init, [new_name, name, processes]) pid = spawn(EventualLeaderElector, :init, [new_name, name, processes])
@@ -657,8 +749,9 @@ defmodule EventualLeaderElector do
register_name(new_name, pid) register_name(new_name, pid)
end end
# Init event must be the first @doc """
# one after the component is created Initializes state and starts the run function
"""
def init(name, parent, processes) do def init(name, parent, processes) do
processes = Enum.map(processes, fn name -> alter_name(name, "_ele") end) processes = Enum.map(processes, fn name -> alter_name(name, "_ele") end)
@@ -674,7 +767,10 @@ defmodule EventualLeaderElector do
run(request_heartbeats(state)) run(request_heartbeats(state))
end end
@doc """
Clears heard_back and updates the sequence number and then sends a request for heartbeats to all proccesses
"""
def request_heartbeats(state) do def request_heartbeats(state) do
state = %{state | heard_back: MapSet.new(), seq: state.seq + 1} state = %{state | heard_back: MapSet.new(), seq: state.seq + 1}
beb_broadcast({:heartbeat_request, state.name, state.seq}, state.processes) beb_broadcast({:heartbeat_request, state.name, state.seq}, state.processes)
@@ -684,15 +780,18 @@ defmodule EventualLeaderElector do
end end
runfn do runfn do
# Answer to a heartbeat_request
{:heartbeat_request, name, seq} -> {:heartbeat_request, name, seq} ->
safecast(name, {:heartbeat, state.parent, seq}) safecast(name, {:heartbeat, state.parent, seq})
state state
# Update heard_back with this name
{:heartbeat, name, seq} -> {:heartbeat, name, seq} ->
or_state seq == state.seq do or_state seq == state.seq do
%{state | heard_back: MapSet.put(state.heard_back, name)} %{state | heard_back: MapSet.put(state.heard_back, name)}
end end
# One time out check the heard_back and send if a new leader is elected inform the parent
{:timeout} -> {:timeout} ->
state = or_state MapSet.size(state.heard_back) >= floor(length(state.processes)/2) + 1 do state = or_state MapSet.size(state.heard_back) >= floor(length(state.processes)/2) + 1 do
to_trust = Enum.at(Enum.sort(MapSet.to_list(state.heard_back)), 0) to_trust = Enum.at(Enum.sort(MapSet.to_list(state.heard_back)), 0)

View File

@@ -1,6 +1,13 @@
defmodule ServerMacros do defmodule ServerMacros do
@moduledoc """
def create_create_loop(name, do: match_exp, else: process_exp) do 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" function_name = :"#{name}_loop"
ast1 = quote do ast1 = quote do
@@ -19,7 +26,6 @@ defmodule ServerMacros do
quote do quote do
defp unquote(function_name)(v, t) do defp unquote(function_name)(v, t) do
var!(v) = v var!(v) = v
unquote(process_exp)
receive do receive do
unquote(ast3) unquote(ast3)
after after
@@ -28,19 +34,11 @@ defmodule ServerMacros do
end end
end end
end end
def create_create_loop(name, do: exp, else: else_exp) do @doc """
create_create_loop(name, do: exp, else: else_exp) this function tries to propose and on failure it calls the else block and
end on success tries to match with the expressions on the do block
"""
def create_create_loop(name, do: exp) do
create_create_loop(name, do: exp, else: nil)
end
defmacro create_loop(name, clauses) do
create_create_loop(name, clauses)
end
defmacro try_propose(val, do: ready, else: recal_do) do defmacro try_propose(val, do: ready, else: recal_do) do
ast1 = quote do ast1 = quote do
{:timeout} -> unquote(recal_do) {:timeout} -> unquote(recal_do)
@@ -68,20 +66,29 @@ defmodule ServerMacros do
end end
defmodule Server do defmodule Server do
@moduledoc """
Contains the to run the server Code
"""
require ServerMacros require ServerMacros
import ServerMacros import ServerMacros
require Utils require Utils
import Utils import Utils
create_log 2 create_log 2
@doc """
Contains the start code for the server
"""
def start(name, participants) do def start(name, participants) do
log("starting server") log("#{name} Starting server")
pid = spawn(Server, :init, [name, participants]) pid = spawn(Server, :init, [name, participants])
register_name(name, pid, false) register_name(name, pid, false)
end end
@doc """
Initializes the state and starts the paxos inspect and then it calls the run fn
"""
def init(name, participants) do def init(name, participants) do
paxos = Paxos.start(alter_name(name, "_paxos"), Enum.map(participants, fn name -> alter_name(name, "_paxos") end), true) paxos = Paxos.start(alter_name(name, "_paxos"), Enum.map(participants, fn name -> alter_name(name, "_paxos") end), true)
state = %{ state = %{
@@ -97,19 +104,24 @@ defmodule Server do
runfn do runfn do
{:start_game, participants, pid_to_inform} -> {:start_game, participants, pid_to_inform} ->
{state, game_id} = try_to_create_game(state, participants) or_state is_list(participants) do
state = set_modifed(state, game_id) {state, game_id} = try_to_create_game(state, participants)
safecast(pid_to_inform, {:start_game_ans, game_id}) state = set_modifed(state, game_id)
state safecast(pid_to_inform, {:start_game_ans, game_id})
state
end
{:get_game_state, game_id, pid_to_inform} -> {:get_game_state, game_id, pid_to_inform} ->
get_game_state(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} -> {:make_move, game_id, move, pid_to_inform} ->
try_to_play_checks(state, game_id, move, pid_to_inform) try_to_play_checks(state, game_id, move, pid_to_inform)
end 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 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 ->
@@ -137,14 +149,30 @@ defmodule Server do
safecast(pid_to_inform, {:make_move, game_id, :player_moved_before, game.game_state, game.hand}) safecast(pid_to_inform, {:make_move, game_id, :player_moved_before, game.game_state, game.hand})
set_modifed(state, game_id) set_modifed(state, game_id)
true -> true ->
try_to_play(state, game_id, move, pid_to_inform) cond do
not is_number(move) ->
safecast(pid_to_inform, {:make_move, game_id, :invalid_move})
state
move >= length(game.game_state) or move < 0 ->
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 end
end end
# 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
new_hand = get_hand_for_game_state(state.games[game_id].game_state)
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} try_propose {:make_move, game_id, name, move, new_hand}
do do
{:decision, {:make_move, ^game_id, ^name, ^move, ^new_hand}} -> {:decision, {:make_move, ^game_id, ^name, ^move, ^new_hand}} ->
@@ -176,7 +204,8 @@ defmodule Server do
try_to_play(state, game_id, move, pid_to_inform) try_to_play(state, game_id, move, pid_to_inform)
end end
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 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 ->
@@ -204,19 +233,23 @@ defmodule Server do
state state
end end
end end
# 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)
if r1 <= 10 do cond do
:+ r1 <= 1 -> :b
else r1 <= 5 -> :-
mx = game_state |> Enum.filter(fn m -> m != :+ end) |> Enum.max() r1 <= 20 -> :+
mn = max(mx - 20, 1) true ->
mx = max(mx - 2, 4) mx = game_state |> Enum.filter(fn m -> m != :+ end) |> Enum.max()
Enum.random(mn..mx) mn = max(mx - 20, 1)
mx = max(mx - 2, 4)
Enum.random(mn..mx)
end end
end end
# 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)
@@ -238,14 +271,16 @@ defmodule Server do
# #
# Utils # Utils
# #
# 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
_ -> false _ -> false
end end
end end
# 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
@@ -253,9 +288,10 @@ defmodule Server do
qurey_status(state) qurey_status(state)
end end
end end
# 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) 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})}
end end
end end
@@ -264,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)
@@ -276,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)
@@ -289,11 +329,12 @@ defmodule Server do
if b == a do if b == a do
case b do case b do
:+ -> simplify_game_state_pluses(tl, indexed_game_state) :+ -> simplify_game_state_pluses(tl, indexed_game_state)
:b -> simplify_game_state_pluses(tl, indexed_game_state)
n -> n ->
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
@@ -310,6 +351,34 @@ defmodule Server do
end end
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 defp is_merged(item) do
case item do case item do
{:merged, _} -> true {:merged, _} -> true
@@ -317,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)
@@ -332,8 +404,8 @@ defmodule Server do
{:merged, _} -> indexed_game_state {:merged, _} -> indexed_game_state
_ -> _ ->
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, 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
@@ -349,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)
@@ -359,27 +432,15 @@ 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) do {:merged, n} -> n
if add do x -> x
remove_merged(tl, rec ++ [n], false) end end)
else
remove_merged(tl, rec, false) # This funcion recieves the game state after the move was done and tries to simplify the game
end
end
defp remove_merged([n | tl], rec, add), do:
remove_merged(tl, rec ++ [n], add)
defp remove_merged(list) do
log("#{inspect(list)}")
remove_merged(list, [], true)
end
defp simplify_game_state(game_state) do defp simplify_game_state(game_state) do
log("game_state: #{inspect(game_state)}")
indexed_game_state = indexed_game_state =
game_state |> game_state |>
reindex(false) reindex(false)
@@ -388,13 +449,12 @@ defmodule Server do
indexed_game_state |> indexed_game_state |>
Enum.filter(fn x -> case x do Enum.filter(fn x -> case x do
{:+, _} -> true {:+, _} -> true
{:b, _} -> true
_ -> false _ -> false
end end
end) |> end) |>
simplify_game_state_pluses(indexed_game_state) simplify_game_state_pluses(indexed_game_state)
log("game_state2: #{inspect(indexed_game_state)}")
if repeat do if repeat do
indexed_game_state |> indexed_game_state |>
Enum.map(fn {v, _} -> v end) |> Enum.map(fn {v, _} -> v end) |>
@@ -404,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
@@ -414,20 +475,30 @@ defmodule Server do
%{state | instance: state.instance + 1 } %{state | instance: state.instance + 1 }
game -> game ->
game_state = game.game_state game_state = game.game_state
{b, e} = Enum.split(game_state, pos_move) game_state = if game.hand[player_name] == :- do
game_state = b ++ [ game.hand[player_name] ] ++ e 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) game_state = simplify_game_state(game_state)
hand = Map.put(game.hand, player_name, new_hand) hand = Map.put(game.hand, player_name, new_hand)
game = %{game| hand: hand, game_state: game_state } game = %{game| hand: hand, game_state: game_state, modified: true }
if length(game.game_state) > 15 do if length(game.game_state) > 16 do
%{state| games: Map.put(state.games, game_id, {:finished, Enum.sum(game.game_state)}), instance: state.instance + 1} %{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 else
%{state| games: Map.put(state.games, game_id, game), instance: state.instance + 1} %{state| games: Map.put(state.games, game_id, game), instance: state.instance + 1}
end end
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] ->
@@ -455,41 +526,47 @@ 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} ->
log("Not Playing in that game")
{:not_playing}
{:game_state, ^v, game_state, hand} ->
log("Got game state, #{inspect(game_state)}, hand: #{inspect(hand)}")
{:state, game_state, hand}
{:game_state, ^v, :not_playing} -> {:game_state, ^v, :not_playing} ->
log("Not Playing in that game") log("Not Playing in that game")
{:not_playing} {:not_playing}
{:game_state, ^v, :game_finished, score} -> {:game_state, ^v, :game_finished, score} ->
log("Game finsihed, #{score}") log("Game finsihed, #{score}")
{:game_finished, 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} -> {:game_state, ^v, :game_does_not_exist} ->
log("Got game does not exist") log("Got game does not exist")
{: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")
@@ -503,11 +580,17 @@ defmodule Server do
{:make_move, ^v, :player_moved_before, game_state, hand} -> {:make_move, ^v, :player_moved_before, game_state, hand} ->
log("Player moved_before, #{inspect(game_state)} #{inspect(hand)}") log("Player moved_before, #{inspect(game_state)} #{inspect(hand)}")
{:player_moved_before, game_state, hand} {:player_moved_before, game_state, hand}
{:make_move, ^v, :invalid_move} ->
log("Invalid Move")
{:invalid_move}
{:make_move, ^v, game_state, hand} -> {:make_move, ^v, game_state, hand} ->
log("Got game state, #{inspect(game_state)}, hand: #{inspect(hand)}") log("Got game state, #{inspect(game_state)}, hand: #{inspect(hand)}")
{: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)
@@ -516,105 +599,281 @@ 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)
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 end
def to_name(list) when is_list(list), do: list |> Enum.map(fn x -> to_name(x) end)
def to_name(atom) when is_atom(atom), do: atom
def to_name(i) do
[ "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)
end
def interpolate(list) do @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] |> [list, 0..length(list) - 1] |>
Enum.zip() |> Enum.zip() |>
Enum.reduce([], fn {v, i}, acc -> acc ++ [i, v] end) |> Enum.reduce([], fn {v, i}, acc -> acc ++ [String.pad_leading("#{i}", 3, " "), v] end)
Enum.map(fn x -> String.pad_leading("#{x}", 2, " ") end)
end end
def printpt(game_state) do # This grows a list with between spaces every item of the interpolated list
rad = 8; # This allows the items to take as puch space in the possilble
printpt1(game_state, 0, rad) 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 end
defp count_char(str, char) do defp fill([], _, acc), do: acc
String.split(str, "") |> Enum.reduce(0, fn x, acc -> if x == char do acc + 1 else acc end end) defp fill([hd | tail], [add_hd | add_tail], acc) do
fill(tail, add_tail ,acc ++ [hd] ++ add_hd)
end end
def printpt1(_, i, rad) when i > rad * 2, do: nil # This functions prints the circle
def printpt1(game_state, i, rad) do defp printpt(game_state, hand), do: printpt(game_state, hand, 0)
res = printpt2(game_state, i, -5, rad, "") defp printpt(_, _, i) when i > 16, do: nil
defp printpt(game_state, hand, i) do
" xxx \n" res = case i do
" xxx yyy \n" 0 ->
" xxx yyy \n" " xxx \n" |>
" xxx yyy \n" String.replace("xxx", Enum.at(game_state, 1))
" xxx yyy \n" 1 ->
" xxx yyy \n" " xxx yyy \n" |>
" xxx yyy \n" String.replace("xxx", Enum.at(game_state, 0)) |>
" xxx yyy \n" String.replace("yyy", Enum.at(game_state, 2))
" xxx yyy \n" 2 ->
" xxx yyy \n" " xxx yyy \n" |>
" xxx yyy \n" String.replace("xxx", Enum.at(game_state, 31)) |>
" xxx yyy \n" String.replace("yyy", Enum.at(game_state, 3))
" xxx yyy \n" 3 ->
" xxx yyy \n" " xxx yyy \n" |>
" xxx yyy \n" String.replace("xxx", Enum.at(game_state, 30)) |>
" xxx \n" 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))
if i != 15 do 5 ->
res = case i do " xxx yyy \n" |>
0 -> String.replace("xxx", Enum.at(game_state, 28)) |>
x = count_char(res, "x") String.replace("yyy", Enum.at(game_state, 6))
log("#{x}") 6 ->
#TODO " xxx yyy \n" |>
res String.replace("xxx", Enum.at(game_state, 27)) |>
16 -> String.replace("yyy", Enum.at(game_state, 7))
#TODO 7 ->
res " xxx yyy \n" |>
v -> String.replace("xxx", Enum.at(game_state, 26)) |>
#TODO String.replace("yyy", Enum.at(game_state, 8))
res 8 ->
end " xxx zzz yyy \n" |>
IO.write(" #{res} \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 end
printpt1(game_state, i + 1, rad)
end
def printpt2(_, _, j, rad, res) when j > rad * 4 + 10, do: res IO.write("#{res}")
def printpt2(game_state, i, j, rad, res) do printpt(game_state, hand, i + 1)
dist = :math.sqrt((i - rad)*(i - rad) + (j / 2 - rad)*(j / 2 - rad));
v = if (dist > rad - 1 and dist < rad + 1) do
case i do
0 -> "x"
16 -> "x"
_ -> if j < rad * 2 do "x" else "y" end
end
else
" "
end
printpt2(game_state, i, j + 1, rad, res <> v)
end end
end end

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"},

191
notes Normal file
View File

@@ -0,0 +1,191 @@
procs = Enum.to_list(1..3) |> Enum.map(fn m -> :"p#{m}")
pids = Enum.map(procs, fn p -> ShopServer.start(p, procs))
pids |> Enum.map(fn p -> Process.exit(p, :kill) end)
max = 100
1..max |> Enum.to_list() |> Enum.reduce(%{}, fn n, acc -> Map.put(acc, n, :not_reseverd) end) |>
" # "
" #1# "
" # "
" # # "
" #0# #2# "
" # # "
" # "
" #4# "
" # "
" # "
" #1# "
" # "
" # # "
" #0# #2# "
" # # "
" # # "
" #7# #3# "
" # # "
" # # "
" #6# #4# "
" # # "
" # "
" #5# "
" # "
" # "
" #1# "
" # "
" # # "
" #0# #2# "
" # # "
" #11 #3# "
" # # "
" # # "
" #10 #4# "
" # # "
" # # "
" #9# #5# "
" # # "
" #8# #6# "
" # # "
" # "
" #7# "
" # "
" # "
" #1# "
" # "
" # # "
" #0# #2# "
" #15 #3# "
" # # "
" # # "
" #14 #4# "
" # # "
" # # "
" #13 #5# "
" # # "
" # # "
" #12 #6# "
" # # "
" # # "
" #11 #7# "
" #10 #8# "
" # # "
" # "
" #9# "
" # "
" # "
" #1# "
" # "
" # # "
" #0# #2# "
" #19 #3# "
" # # "
" # # "
" #18 #4# "
" #17 #5# "
" # # "
" # # "
" #16 #6# "
" # # "
" #15 #7# "
" # # "
" #14 #8# "
" # # "
" # # "
" #13 #9# "
" #12 #10 "
" # # "
" # "
" #11 "
" # "
" # "
" #1# "
" # "
" # # "
" #0# #2# "
" #23 #3# "
" # # "
" #22 #4# "
" #21 #5# "
" # # "
" #20 #6# "
" # # "
" # # "
" #19 #7# "
" # # "
" # # "
" #18 #8# "
" # # "
" #17 #9# "
" #16 #10 "
" # # "
" #15 #11 "
" #14 #12 "
" # # "
" # "
" #13 "
" # "