Compiled on: 2025-06-30 — printable version
https://en.wikipedia.org/wiki/Pong
In this running example we are going to
in order to exemplify the development of a distributed system project.
Here’s an overview of the final result:
Source code at: https://github.com/unibo-fc-isi-ds/dpongpy
Go on, play with it!
pip install dpongpy
python -m dpongpy --help # to see all options
python -m dpongpy --mode local # to play offline
# to start a centralised server
python -m dpongpy --mode centralised --role coordinator
python -m dpongpy --mode centralised --role terminal --side left --keys wasd --host IP_COORDINATOR
python -m dpongpy --mode centralised --role terminal --side right --keys arrows --host IP_COORDINATOR
Default key bindings:
Recall to
pip uninstall dpongpy
before proceeding with this lecture, to avoid conflicts with your working copy
PyGame is a popular Python library for writing simple games
It handles many game development tasks, such as graphics, sound, time, and input management.
Simple to use, yet powerful enough to create non-trivial games
Lightweight, portable, and easy to install
pip install pygame
(better to use a virtual environment)A game loop is the main logic cycle of most video-games
It continuously runs while the game is active, managing the following aspects:
Most commonly, some wait is introduced at the end of each cycle
import pygame
# Initialize Pygame
pygame.init()
# Screen dimensions
screen_size = pygame.Vector2(800, 600)
screen = pygame.display.set_mode(screen_size)
pygame.display.set_caption("Game Loop Example")
# Colours
white = pygame.color.Color(255, 255, 255) # White color RGB
black = pygame.color.Color(0, 0, 0) # Black color RGB
# Circle settings
circle_radius = min(screen_size) / 10 # Circle radius is 1/10th of the screen size's smallest dimension
circle_posistion = screen_size / 2 # Start at the center
circle_speed = min(screen_size) / 10 # Absolute speed of the circle is 1/10th of the screen size's smallest dimension
circle_velocity = pygame.Vector2(0, 0) # Initial velocity of the circle
clock = pygame.time.Clock()
# Main game loop
dt = 0 # time elapsed since last loop iteration
running = True # game should stop when set to False
while running:
# Event handling
for event in pygame.event.get([pygame.KEYDOWN, pygame.KEYUP]):
if event.key == pygame.K_ESCAPE:
running = False # Quit the game
elif event.key == pygame.K_w: # Move up
circle_velocity.y = -circle_speed if event.type == pygame.KEYDOWN else 0
elif event.key == pygame.K_s: # Move down
circle_velocity.y = circle_speed if event.type == pygame.KEYDOWN else 0
elif event.key == pygame.K_a: # Move left
circle_velocity.x = -circle_speed if event.type == pygame.KEYDOWN else 0
elif event.key == pygame.K_d: # Move right
circle_velocity.x = circle_speed if event.type == pygame.KEYDOWN else 0
# Clear screen
screen.fill(black) # Black background
# Update and logs the circle's position
old_circle_posistion = circle_posistion.copy()
circle_posistion += dt * circle_velocity
if old_circle_posistion != circle_posistion:
print("Circle moves from", old_circle_posistion, "to", circle_posistion)
# Draw the circle
pygame.draw.circle(screen, white, circle_posistion, circle_radius)
# Update the display
pygame.display.flip()
# Ensure the game runs at 60 FPS
dt = clock.tick(60) / 1000 # Time elapsed since last loop iteration in seconds
# Quit Pygame
pygame.quit()
full example on GitHub
PyGame comes with a notion of events and event queue
each event has a type (e.g., pygame.KEYDOWN
and pygame.KEYUP
) and attributes (e.g., event.key
)
pygame.event.Event
int
egersevents can be retrieved from the queue (e.g., pygame.event.get([RELEVANT_TYPES])
)…
RELEVANT_TYPES
is a list of event types to be retrieved… and possibly provoked (i.e. appended to the queue) by the programmer (e.g., pygame.event.post(event)
)
event
is an instance of pygame.event.Event
from pygame.math import Vector2
from pygame.rect import Rect
class GameObject:
def __init__(self, size, position=None, speed=None, name=None):
self.size = Vector2(size)
self.position = Vector2(position) if position is not None else Vector2()
self.speed = Vector2(speed) if speed is not None else Vector2()
self.name = name or self.__class__.__name__.lower()
def __eq__(self, other):
return isinstance(other, type(self)) and \
self.name == other.name and \
self.size == other.size and \
self.position == other.position and \
self.speed == other.speed
def __hash__(self):
return hash((type(self), self.name, self.size, self.position, self.speed))
def __repr__(self):
return f'<{type(self).__name__}(id={id(self)}, name={self.name}, size={self.size}, position={self.position}, speed={self.speed})>'
def __str__(self):
return f'{self.name}#{id(self)}'
@property
def bounding_box(self):
return Rect(self.position - self.size / 2, self.size)
def update(self, dt):
self.position = self.position + self.speed * dt
if __name__ == '__main__':
x = GameObject((10, 20), (100, 200), (1, 2), 'myobj')
assert x.size == Vector2(10, 20)
assert x.position == Vector2(100, 200)
assert x.speed == Vector2(1, 2)
assert x.name == 'myobj'
assert x.bounding_box.topleft == (95, 190)
assert x.bounding_box.size == (10, 20)
assert x.bounding_box.bottomright == (105, 210)
assert str(x) == 'myobj#' + str(id(x))
assert repr(x) == ('<GameObject(id=%d, name=myobj, size=[10, 20], position=[100, 200], speed=[1, 2])>' % id(x))
y = GameObject((10, 20), (100, 200), (1, 2), 'myobj')
z = GameObject((10, 20), (100, 200), (1, 2), 'myobj2')
assert x == y
assert x != z
x.update(2)
assert x.position == Vector2(102, 204)
assert x != y
class GameObject { + name: str + position: Vector2 + size: Vector2 + speed: Vector2 + bounding_box: Rect + update(dt: float) }
package “pygame” { class Vector2 { + x: float + y: float } class Rect { + left: float + top: float + width: float + height: float } }
GameObject *-d- Vector2 GameObject *– Rect
full example on GitHub
MOVE_UP
, MOVE_DOWN
, STOP
, etc.pygame.event.custom_type()
import pygame
from pygame.event import Event, custom_type
from enum import Enum
class GameEvent(Enum):
MOVE_UP: int = custom_type()
MOVE_DOWN: int = custom_type()
MOVE_LEFT: int = custom_type()
MOVE_RIGHT: int = custom_type()
STOP: int = pygame.QUIT
def create_event(self, **kwargs):
return Event(self.value, kwargs)
@classmethod
def all(cls) -> set['GameEvent']:
return set(cls.__members__.values())
@classmethod
def types(cls) -> list[int]:
return [event.value for event in cls.all()]
KEYMAP_WASD = {
pygame.K_w: GameEvent.MOVE_UP,
pygame.K_s: GameEvent.MOVE_DOWN,
pygame.K_d: GameEvent.MOVE_RIGHT,
pygame.K_a: GameEvent.MOVE_LEFT,
pygame.K_ESCAPE: GameEvent.STOP
}
class InputHandler:
def __init__(self, keymap=None):
self.keymap = dict(keymap) if keymap is not None else KEYMAP_WASD
def handle_inputs(self):
for event in pygame.event.get([pygame.KEYDOWN, pygame.KEYUP]):
if event.key in self.keymap.keys():
game_event = self.keymap[event.key].create_event(up=event.type == pygame.KEYUP)
self.post_event(game_event)
def post_event(self, game_event):
pygame.event.post(game_event)
class Controller(InputHandler):
def __init__(self, game_object, speed, keymap=None):
super().__init__(keymap)
self._game_object = game_object
self._speed = speed
def update(self, dt):
for event in pygame.event.get(GameEvent.types()):
self._update_object_according_to_event(self._game_object, event)
self._game_object.update(dt)
def _update_object_according_to_event(self, game_object, event):
match GameEvent(event.type):
case GameEvent.MOVE_UP:
game_object.speed.y = 0 if event.up else -self._speed
case GameEvent.MOVE_DOWN:
game_object.speed.y = 0 if event.up else self._speed
case GameEvent.MOVE_LEFT:
game_object.speed.x = 0 if event.up else -self._speed
case GameEvent.MOVE_RIGHT:
game_object.speed.x = 0 if event.up else self._speed
case GameEvent.STOP:
pygame.quit()
exit()
top to bottom direction
enum GameEvent { + MOVE_UP + MOVE_DOWN + MOVE_LEFT + MOVE_RIGHT + STOP + create_event(**kwargs): Event + {static} all(): set[GameEvent] + {static} types(): list[int] }
package “pygame” { package “event” { class Event { + type: int + data: dict } } }
class InputHandler { + keymap: Dict[int, int] + handle_inputs() -> List[int] + post_event(event: Event) }
class Controller { - _game_object: GameObject - _speed: float + update(dt: float) - _update_object_according_to_event(game_object, event) }
InputHandler <|-d- Controller
InputHandler .u.> GameEvent: uses InputHandler .u.> Event: uses
full example on GitHub
import pygame
class View:
def __init__(self, game_object, screen=None, size=None, background_color=None, foreground_color=None):
if screen is not None:
size = screen.get_size()
self._size = size or (800, 600)
self.background_color = pygame.Color(background_color or "black")
self.foreground_color = pygame.Color(foreground_color or "white")
self._screen = screen or pygame.display.set_mode(self._size)
assert game_object is not None, "game_object cannot be None"
self._game_object = game_object
def render(self):
self._reset_screen(self._screen, self.background_color)
self._draw_game_object(self._game_object, self.foreground_color)
pygame.display.flip()
def _reset_screen(self, screen, color):
screen.fill(color)
def _draw_game_object(self, game_object, color):
pygame.draw.ellipse(self._screen, color, game_object.bounding_box)
top to bottom direction
class View { - _size: tuple[int, int] - _screen: Surface + background_color: Color + foreground_color: Color - _game_object: GameObject + render() - _reset_screen(screen, color) - _draw_game_object(game_object, color) }
package “pygame” { class Surface { + fill(color: Color) + flip() } class Color { + r: int + g: int + b: int + a: int } }
View .d.> Surface: uses View ..> Color: uses
full example on GitHub
InputHandler
Controller
View
import pygame
from .example2_game_object import GameObject
from .example3_controller import Controller
from .example4_view import View
# Initialize Pygame
pygame.init()
# Screen dimensions
screen_size = pygame.Vector2(800, 600)
screen = pygame.display.set_mode(screen_size)
pygame.display.set_caption("Game Loop Example")
# Game objects
circle = GameObject(size=screen_size / 10, position=screen_size / 2, name="circle")
# Wire game-loop components together
controller = Controller(game_object=circle, speed=min(screen_size) / 10)
view = View(game_object=circle, screen=screen)
clock = pygame.time.Clock()
# Main game loop
dt = 0 # time elapsed since last loop iteration
running = True # game should stop when set to False
while running:
controller.handle_inputs()
controller.update(dt)
view.render()
# Ensure the game runs at 60 FPS
dt = clock.tick(60) / 1000 # Time elapsed since last loop iteration in seconds
# Quit Pygame
pygame.quit()
full example on GitHub
Pong
game model comprehends:
GameObject
: visible entity in the game
name
, position
, size
, *speed
*, bounding_box
position
, size
, and *speed
* are Vector2
instancesPaddle
, Ball
Paddle
instances are assigned to one side
(property) of the screen
Direction
s: UP
, DOWN
, LEFT
, RIGHT
Board
: the plane upon which the game is played (black rectangle in the figure)
size
, walls
Wall
: invisible entity that reflects the Ball
when hitAncillary classes: Vector2
, Rectangle
, Direction
Vector2
utility class from PyGame, representing a 2D vectorRectangle
utility class, representing a rectangle, supporting collision detectionDirection
is an enumeration of 4 possible directions + NONE
(lack of direction)left to right direction
package “dpongpy.model” {
enum Direction {
+ {static} NONE
+ {static} LEFT
+ {static} UP
+ {static} RIGHT
+ {static} DOWN
--
+ is_vertical: bool
+ is_horizontal: bool
+ {static} values(): list[Direction]
}
interface Sized {
+size: Vector2
+width: float
+height: float
}
interface Positioned {
+position: Vector2
+x: float
+y: float
}
class Rectangle {
+ top_left: Vector2
+ top_right: Vector2
+ bottom_right: Vector2
+ bottom_left: Vector2
+ top: float
+ bottom: float
+ left: float
+ right: float
+ corners -> list[Vector2]
+ overlaps(other: Rectangle) -> bool
+ is_inside(other: Rectangle) -> bool
+ intersection_with(other: Rectangle) -> Rectangle
+ hits(other: Rectangle) -> dict[Direction, float]
}
Rectangle --|> Sized
Rectangle --|> Positioned
class GameObject {
+ name: str
+ speed: Vector2
+ bounding_box: Rectangle
+ update(dt: float)
+ override(other: GameObject)
}
GameObject --|> Sized
GameObject --|> Positioned
GameObject "1" *-- "1" Rectangle
class Paddle {
+ side: Direction
}
Paddle --|> GameObject
Paddle "1" *-- "1" Direction
class Ball
Ball --|> GameObject
class Board {
+ walls: dict[Direction, GameObject]
}
Board --|> Sized
Board "1" *-- "4" Direction
Board "1" *-- "4" GameObject
class Pong {
+ config: Config
+ random: Random
+ ball: Ball
+ paddles: list[Paddle]
+ board: Board
+ updates: int
+ time: float
--
+ reset_ball(speed: Vector2 = None)
+ add_paddle(side: Direction, paddle: Paddle = None)
+ paddle(side: Direction): Paddle
+ has_paddle(side: Direction): bool
+ remove_paddle(self, Direction)
--
+ update(dt: float)
+ move_paddle(paddle: int|Direction, direction: Direction)
+ stop_paddle(paddle: int|Direction):
+ override(self, other: Pong):
- _handle_collisions(subject, objects)
}
'Pong --|> Sized
Pong "1" *-- "1" Ball
Pong "1" *-- "4" Paddle
Pong "1" *-- "1" Board
Pong "1" *-- "1" Config
note top of Pong
model class
end note
class Config {
+ paddle_ratio: Vector2
+ ball_ratio: float
+ ball_speed_ratio: float
+ paddle_speed_ratio: float
+ paddle_padding: float
}
}
package “random” { class Random { + uniform(a: float, b: float): float } }
package “pygame” { class Vector2 { +x: float +y: float } }
Rectangle “1” *-l- “6” Vector2 Pong “1” *– “1” Random
code on GitHub
Facilities of the Pong
class, to configure the game:
reset_ball(speed=None)
: re-locates the Ball
at the center of the Board
, setting its speed
vector to the given value
speed
is None
add_paddle(side, paddle=None)
: assigns a Paddle
to the Pong
, at the given side
(if not already present)
Paddle
is created from scratch if paddle
is None
side
of the Board
paddle(side)
: retrieves the Paddle
at the given side
has_paddle(side)
: checks if a Paddle
is present at the given side
remove_paddle(side)
: removes the Paddle
at the given side
Facilities of the Pong
class, to animate the game:
update(dt)
: updates the game state, moving the Ball
and the Paddles
according to the given time delta
Ball
and the Paddles
and the Walls
_handle_collisions
method to the purposemove_paddle(side, direction)
: moves the selected Paddle
in the given direction
by setting its speed
vector accordingly
paddle
can either be an int
(index of the Paddle
in the paddles
list) or a Direction
(side of the Paddle
)stop_paddle(side)
: stops the selected Paddle
from movingCollision detection is a crucial aspect of game development
In Pong
collisions are very simple, as they simply rely on the game objects’ bounding boxes
In Pong
there are 3 sorts of relevant collisions:
Ball
vs. Paddle
Ball
vs. Wall
Paddle
vs. Wall
Bouncing can simply be achieved by reversing the Ball
’s speed vector along the colliding axis
GameObject
s (non overlapping)GameObject
s (overlapping)GameObject
s (inside)Ball
is close to an obstacle and update()
is calledBall
is updated, it is now overlapping an obstacle (wall, or paddle)Bouncing
= reversing the speed vector along the colliding axis + re-locating the Ball
outside the obstacleupdate()
call will move the Ball
away from the obstacleInsight: inputs are external data, representing events that may impact the system state
(most commonly corresponding to users’ actions)
(exceptions apply)
In a simple video game like Pong, we distinguish between:
What input events are relevant for the software, during its execution?
Paddle
’s movementPaddle
’s stopWhat other control events are relevant for the software, during its execution?
InputHandler
interprets keyboard input events and generates control eventsEventHandler
processes control events and updates the game state (Pong
class) accordinglyInputHandler
is also in charge of generating time passing eventsPaddle
s are moved by players, via the keyboard
when players are distributed, the keyboards are different
when players are local, there’s only one keyboard
PlayerAction
enumerates all possible actions a player can perform (on a paddle)
Direction
, stop paddle, quit the gameActionMap
, associating key codes to PlayerAction
sleft to right direction
package “pygame.event” { class Event { + type: int + dict: dict } }
package “dpongpy.controller” { class ActionMap { + move_up: int + move_down: int + move_left: int + move_right: int + quit: int + name: str }
enum PlayerAction {
+ {static} MOVE_UP
+ {static} MOVE_DOWN
+ {static} MOVE_RIGHT
+ {static} MOVE_LEFT
+ {static} STOP
+ {static} QUIT
}
enum ControlEvent {
+ {static} PLAYER_JOIN
+ {static} PLAYER_LEAVE
+ {static} GAME_START
+ {static} GAME_OVER
+ {static} PADDLE_MOVE
+ {static} TIME_ELAPSED
}
interface InputHandler {
+ create_event(event, **kwargs)
+ post_event(event, **kwargs)
..
+ key_pressed(key: int)
+ key_released(key: int)
+ time_elapsed(dt: float)
..
+ handle_inputs(dt: float)
}
interface EventHandler {
+ handle_events()
..
+ on_player_join(pong: Pong, paddle):
+ on_player_leave(pong: Pong, paddle):
+ on_game_start(pong: Pong):
+ on_game_over(pong: Pong):
+ on_paddle_move(pong: Pong, paddle, direction):
+ on_time_elapsed(pong: Pong, dt):
}
}
package “dpongpy.model” { class Pong { stuff } }
ActionMap <.. InputHandler: knows Event <.. InputHandler: processes InputHandler ..> PlayerAction: selects\nbased on\nActionMap\nand\nEvent InputHandler ..> ControlEvent: generates PlayerAction <.. ControlEvent: wraps ControlEvent <.. EventHandler: processes EventHandler ..> Pong: updates\nbased on\nControlEvent
Paddle
and to an ActionMap
to govern that Paddle
ActionMap
is a dictionary mapping key codes to PlayerAction
s
pygame.K_UP
$\rightarrow$ MOVE_UP
, pygame.K_DOWN
$\rightarrow$ MOVE_DOWN
for right paddle (arrow keys)pygame.K_w
$\rightarrow$ MOVE_UP
, pygame.K_s
$\rightarrow$ MOVE_DOWN
for left paddle (WASD keys)PlayerAction
s are one particular sort of ControlEvent
s that may occur in the game
GAME_START
, PADDLE_MOVE
, TIME_ELAPSED
, etc.ControlEvent
s are custom PyGame events which animate the game
PADDLE_MOVE
carries the information about which Paddle
is moving and where it is movingpackage “dpongpy.controller” { interface EventHandler interface InputHandler interface Controller { - _pong: Pong }
EventHandler <|-d- Controller InputHandler <|-d- Controller }
we call Controller
any entity which acts as both an EventHandler
and an InputHandler
Insight: outputs are representations of the system state
that are perceived by the external world
(most commonly corresponding to visual or auditory feedback for the user)
In our case:
Pong
, control $\approx$ EventHandler
+ InputHandler
@startuml package “dpongpy” { class Pong
package “view” { interface PongView { - _pong: Pong + render() }
interface ShowNothingPongView
note top of ShowNothingPongView useful for hiding the game end note
interface ScreenPongView { - _screen: Surface + render_ball(ball: Ball) + render_paddles(paddle: Iterable[Paddle]) + render_paddle(paddle: Paddle) }
PongView <|-d- ScreenPongView PongView <|-d- ShowNothingPongView }
PongView *-u- Pong } @enduml
import pygame
from dpongpy.model import *
from typing import Iterable
def rect(rectangle: Rectangle) -> pygame.Rect:
return pygame.Rect(rectangle.top_left, rectangle.size)
class PongView:
def __init__(self, pong: Pong):
self._pong = pong
def render(self):
raise NotImplemented
class ShowNothingPongView(PongView):
def render(self):
pass
class ScreenPongView(PongView):
def __init__(self, pong: Pong, screen: pygame.Surface = None):
super().__init__(pong)
self._screen = screen or pygame.display.set_mode(pong.size)
def render(self):
self._screen.fill("black")
self.render_ball(self._pong.ball)
self.render_paddles(self._pong.paddles)
def render_ball(self, ball: Ball):
pygame.draw.ellipse(self._screen, "white", rect(ball.bounding_box), width=0)
def render_paddles(self, paddles: Iterable[Paddle]):
for paddle in paddles:
self.render_paddle(paddle)
def render_paddle(self, paddle: Paddle):
pygame.draw.rect(self._screen, "white", rect(paddle.bounding_box), width=0)
render()
methodtop to bottom direction package “dpongpy” { class Settings { + config: Config + size: tuple[int] + fps: int + initial_paddles: tuple[Direction] }
class PongGame { + settings: Settings + pong: Pong + dt: float + clock: pygame.time.Clock + running: bool + view: PongView + controller: Controller .. + create_view(): PongView + create_controller(): Controller .. + before_run() + after_run() + at_each_run() .. + run() + stop() } Settings -d[hidden]- PongGame }
class PongGame:
def __init__(self, settings: Settings = None):
self.settings = settings or Settings()
self.pong = Pong(
size=self.settings.size,
config=self.settings.config,
paddles=self.settings.initial_paddles
)
self.dt = None
self.view = self.create_view()
self.clock = pygame.time.Clock()
self.running = True
self.controller = self.create_controller(settings.initial_paddles)
def create_view(self):
from dpongpy.view import ScreenPongView
return ScreenPongView(self.pong, debug=self.settings.debug)
def create_controller(game, paddle_commands: dict[Direction, ActionMap]):
from dpongpy.controller.local import PongLocalController
class Controller(PongLocalController):
def __init__(self, paddle_commands):
super().__init__(game.pong, paddle_commands)
def on_game_over(this, _):
game.stop()
return Controller(paddle_commands)
def before_run(self):
pygame.init()
def after_run(self):
pygame.quit()
def at_each_run(self):
pygame.display.flip()
def run(self):
try:
self.dt = 0
self.before_run()
while self.running:
self.controller.handle_inputs(self.dt)
self.controller.handle_events()
self.view.render()
self.at_each_run()
self.dt = self.clock.tick(self.settings.fps) / 1000
finally:
self.after_run()
def stop(self):
self.running = False
def main(settings = None):
if settings is None:
settings = Settings()
PongGame(settings).run()
notice the implementation of run()
See the command line options via poetry run python -m dpongpy -h
:
pygame 2.6.0 (SDL 2.28.4, Python 3.12.5)
Hello from the pygame community. https://www.pygame.org/contribute.html
usage: python -m dpongpy [-h] [--mode {local}] [--side {none,left,up,right,down}] [--keys {wasd,arrows,ijkl,numpad}] [--debug] [--size SIZE SIZE] [--fps FPS]
options:
-h, --help show this help message and exit
mode:
--mode {local}, -m {local}
Run the game in local or centralised mode
game:
--side {none,left,up,right,down}, -s {none,left,up,right,down}
Side to play on
--keys {wasd,arrows,ijkl,numpad}, -k {wasd,arrows,ijkl,numpad}
Keymaps for sides
--debug, -d Enable debug mode
--size SIZE SIZE, -S SIZE SIZE
Size of the game window
--fps FPS, -f FPS Frames per second
poetry run python -m dpongpy --mode local
(only DS related aspects are discussed here)
In other words, how is the infrastructure of the system organized?
Let’s focus on information flow:
Let’s suppose a central server is coordinating the game:
Let’s suppose a central server is coordinating the game, but a broker is used to relay messages:
Like the centralised one, but the server is replicated and a consensus protocol is used to keep replicas in sync:
PongGame
entity updated by the server and replicated on all clients, in a master-slave fashionPongView
entity per client, to render the game state on the local screenInputHandler
entity per client, to grasp local inputs and send them to the serverEventHandler
entity on the server, to receive remote inputs and update the PongGame
entity accordingly
or
Coordinator is the central server, coordinating the game
Terminal is the client’s end point, visualising the game
Event-Based Architecture:
Aren’t we missing something?
How should players join the game?
How should players leave the game?
Assumptions
Joining:
PLAYER_JOIN
message to the coordinator upon startingLeaving (gracefully):
ESC
) to quit the gamePLAYER_LEAVE
message to the coordinator upon quitting
what if the coordinator is not available when the terminal starts?
what if a terminal crashes before sending the PLAYER_LEAVE
message?
how could the coordinator distinguish between a crashed terminal and one sending no inputs?
what if the coordinator crashes while some terminals are still running?
what if a terminal selects a side that is already taken?
what if a terminal send inputs concerning the wrong paddle?
what if the coordinator is not available when the terminal starts?
what if a terminal crashes before sending the PLAYER_LEAVE
message?
how could the coordinator distinguish between a crashed terminal and one sending no inputs?
what if the coordinator crashes while some terminals are still running?
what if a terminal selects a side that is already taken?
PLAYER_JOIN
messagePLAYER_JOIN
message and the terminal terminates
what if a terminal send inputs concerning the wrong paddle?
Let’s focus on the currently available implementation:
https://github.com/unibo-fc-isi-ds/dpongpy
package dpongpy
├── class PongGame // entry point for local game
├── package dpongpy.model
│ └── class Pong
├── package dpongpy.controller
│ ├── interface EventHandler
│ ├── interface InputHandler
│ ├── package dpongpy.local
│ │ ├── class PongInputHandler : InputHandler
│ │ ├── class PongEventHandler : EventHandler
│ │ └── class PongLocalController : PongInputHandler,PongEventHandler
│ └── package dpongpy.view
│ ├── interface PongView
│ ├── class ScreenPongView : PongView
│ └── class ShowNothingPongView : PongView
├── package dpongpy.remote
│ ├── class Address
│ ├── interface Session
│ ├── interface Server
│ ├── interface Client
│ ├── package dpongpy.remote.centralised
│ │ ├── class PongCoordinator : PongGame // entry point for remote game, coordinator side
│ │ └── class PongTerminal : PongGame // entry point for remote game, terminal side
│ ├── package dpongpy.remote.udp
│ │ ├── class UdpSession : Session
│ │ ├── class UdpServer : Server
│ │ └── class UdpClient : Client
│ └── package dpongpy.remote.presentation
│ ├── Serializer
│ └── Deserializer
(only the organization of classes and interfaces into packages is reported, as well as sub-typing relationships)
Give a look to the code, especially the PongCoordinator
and PongTerminal
classes
compare the code to the design we discussed, especially the interaction and behavioural views
W.r.t. the previous corner cases, in which situations are we?
what if the coordinator is not available when the terminal starts?
what if a terminal crashes before sending the PLAYER_LEAVE
message?
what if the coordinator crashes while some terminals are still running?
what if a terminal selects a side that is already taken?
what if a terminal send inputs concerning the wrong paddle?
class Controller(PongInputHandler, EventHandler): # dpongpy/remote/centralised/__init__.py, line 114
def post_event(self, event: Event | ControlEvent, **kwargs):
event = super().post_event(event, **kwargs)
if ControlEvent.PADDLE_MOVE.matches(event): # this is how you can inject the bug
event.dict["paddle_index"] = Direction.LEFT
...
Are we prioritizing availability or consistency with the current design/implementation?
Let’s try to simulate a network partition:
UDP_DROP_RATE
environment variable to a number $p \in [0, 1]$ (say, 0.2
) to affect the probability of a package being dropped to $p$…
20%
in our case)is the gameplay fluid or laggy?
Goal: modify the current dpongpy
implementation to prioritize availability over consistency, in order to make the game fluid, even in presence of network issues
Hints: implement speculative execution on the terminal-side, to make the game appear available even when the coordinator non-responsive
Deadline: two weeks from today
Incentive: +1 point on the final grade (if solution is satisfying)
Submission:
dpongpy
repositoryexercise-lab1
[A.Y. 2024/2025 Surname, Name] Exercise: Available Distributed Pong
Compiled on: 2025-06-30 — printable version