Distributed Systems Coursework
This repository has been archived on 2024-01-29. You can view files and clone it, but cannot push or open issues or pull requests.
Go to file
Andre Henriques 05c09d10f3
All checks were successful
continuous-integration/drone/push Build is passing
fix comments
2024-01-21 19:48:43 +00:00
lib fix comments 2024-01-21 19:48:43 +00:00
presentation chore: fix presentation 2024-01-09 15:03:12 +00:00
.drone.yml fixed ci 2024-01-19 18:50:22 +00:00
.formatter.exs Initial commit 2023-11-16 11:57:42 +00:00
.gitignore added logs to gitignore 2024-01-08 15:55:39 +00:00
mix.exs Paxos version 1 2023-11-28 21:42:16 +00:00
notes Fixed the funk 2024-01-18 22:28:10 +00:00
README.md Merge branch 'main' of git.andr3h3nriqu3s.com:andr3/distributed_system_coursework 2024-01-21 18:17:58 +00:00

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. 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:

    {: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:

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:

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:

    {: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:

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:

    {: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:

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:

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:

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:

# In c1
Node.connect(:"c2@<computer_name>")

On both terminals compile paxos and server:

# In both run
c "paxos.ex"; c "server.ex"

Start the servers on the first terminal, create a game, and enter display mode:

# 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:

# 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!