Distributed Pong


Distributes Systems — Module 2

A.Y. 2024/2025

Giovanni Ciatto



Compiled on: 2025-06-30 — printable version

back to ToC

What’s Pong?

https://en.wikipedia.org/wiki/Pong

Pong screenshot

Goal

In this running example we are going to

  • implement a distributed version of the classic game Pong
  • in Python
  • using PyGame

in order to exemplify the development of a distributed system project.

Spoiler

Here’s an overview of the final result:

dpongpy screenshot

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:

  • Left paddle: WASD
  • Right paddle: Arrow keys

Recall to pip uninstall dpongpy before proceeding with this lecture, to avoid conflicts with your working copy

What is PyGame?

  • 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)

Preliminaries

The Game Loop

What is a Game Loop?

  • A game loop is the main logic cycle of most video-games

  • It continuously runs while the game is active, managing the following aspects:

    1. Processing user input (e.g., keyboard, mouse, gamepad)
    2. Updating the game state (e.g., moving objects in the virtual space)
    3. Rendering the game (e.g., drawing the game world on the screen)
    4. Simulating the passage of time in the game (e.g., moving objects even in absence of inputs)
  • Most commonly, some wait is introduced at the end of each cycle

    • to control the game’s frame rate

The Game Loop in PyGame

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

Aspects to notice (cf. PyGame documentation):

  • PyGame comes with a notion of events and event queue

    • this is good for handling user inputs
  • each event has a type (e.g., pygame.KEYDOWN and pygame.KEYUP) and attributes (e.g., event.key)

    • there is a class for events, namely pygame.event.Event
    • event types are indeed integers
  • events can be retrieved from the queue (e.g., pygame.event.get([RELEVANT_TYPES]))…

    • where 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))

    • where event is an instance of pygame.event.Event

Clean Code Recommendations

  1. Better to explicitly represent game objects in the code
    • Game Object $\approx$ any entity that may appear in the game world (e.g. the circle)
      • interesting aspects: size, position , speed, name, etc.
      • may have methods to update its state
    • the overall game state consists of all game objects therein contained
      • $\Rightarrow$ update the game $\equiv$ update each game objects
    • cf. PyGame documentation
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

Clean Code Recommendations

  1. Better to explicitly represent input handlers and controllers in the code
    • Input Handler $\approx$ any entity that may interpret user inputs and map them to game events
      • supports plugging in different key maps
    • Game Events $\approx$ represent actions that make sense for game objects
    • Controller $\approx$ any entity that may interpret game events and update the game state accordingly
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

Clean Code Recommendations

  1. Better to delegate rendering to a dedicated class taking care of the view of the game
    • View $\approx$ any entity that may draw game objects on the screen
      • may have methods to render the game (objects) on the screen
    • cf. PyGame documentation
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

Clean Code Recommendations

  1. Wrap up
    • game loop is now much simplified
    • changes in input handling $\rightarrow$ implement new InputHandler
    • changes in game logic $\rightarrow$ implement new Controller
    • changes in visualization $\rightarrow$ implement new 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 Model

Let’s infer a model from the view (pt. 1)

Pong view for 4 players

  • up to 4 paddles for as many players
  • a ball that bounces off the paddles and the walls

Let’s infer a model from the view (pt. 2)

Detail on the visible and invisible aspects of the Pong model


Pong game model comprehends:

  1. GameObject: visible entity in the game

    • relevant properties: name, position, size, *speed*, bounding_box
      • position, size, and *speed* are Vector2 instances
    • particular cases: Paddle, Ball
      • Paddle instances are assigned to one side (property) of the screen
        • 4 possible sides for as many Directions: UP, DOWN, LEFT, RIGHT
        • each paddle corresponds to a different player
          • to support local multiplayer, different key bindings are needed
  2. Board: the plane upon which the game is played (black rectangle in the figure)

    • relevant properties: size, walls
    • Wall: invisible entity that reflects the Ball when hit
  3. Ancillary classes: Vector2, Rectangle, Direction

    • Vector2 utility class from PyGame, representing a 2D vector
    • Rectangle utility class, representing a rectangle, supporting collision detection
    • Direction is an enumeration of 4 possible directions + NONE (lack of direction)

Let’s infer a model from the view (pt. 3)

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

Let’s infer a model from the view (pt. 4)

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
    • random speed direction is provided if speed is None
  • add_paddle(side, paddle=None): assigns a Paddle to the Pong, at the given side (if not already present)
    • the Paddle is created from scratch if paddle is None
      • in this case, the paddle is centered on the 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
    • computes collisions between the Ball and the Paddles and the Walls
      • uses _handle_collisions method to the purpose
  • move_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)
    • left and right paddles can only move up and down, respectively
    • up and down paddles can only move left and right, respectively
  • stop_paddle(side): stops the selected Paddle from moving

About collisions

  • Collision detection is a crucial aspect of game development

    • it is the process of determining when two or more game objects overlap
    • it is the basis for physics simulation in games
  • In Pong collisions are very simple, as they simply rely on the game objects’ bounding boxes

    • a bounding box the minimal rectangle that encloses the game object
    • a collision is detected when two bounding boxes overlap
  • In Pong there are 3 sorts of relevant collisions:

    1. Ball vs. Paddle
    2. Ball vs. Wall
    3. Paddle vs. Wall
  • Bouncing can simply be achieved by reversing the Ball’s speed vector along the colliding axis

Collision detection in GameObjects (non overlapping)

Collision detection in GameObjects (overlapping)

Collision detection in GameObjects (inside)

Bouncing

  1. Suppose the Ball is close to an obstacle and update() is called

Bouncing

  1. When the position of the Ball is updated, it is now overlapping an obstacle (wall, or paddle)

Bouncing

  1. Bouncing = reversing the speed vector along the colliding axis + re-locating the Ball outside the obstacle

Bouncing

  1. Another update() call will move the Ball away from the obstacle

Pong I/O

About Inputs

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

  • control events: corresponding to some update in the game state
  • input events: low-level events which require some processing to be translated into control events

Input handling and Control Events (pt. 1)

Design questions

  1. What input events are relevant for the software, during its execution?

    • keyboard inputs, in particular pressures and releases of keys
      • pressure should provoke a Paddle’s movement
      • release should provoke a Paddle’s stop
  2. What other control events are relevant for the software, during its execution?

    • player joining or leaving the game (mostly useful in distributed setting)
    • game starting or ending (mostly useful in distributed setting)
    • time passing in the game
    • paddle moving or stopping

Design choices

  • We introduce two abstractions to handle inputs and control events:
    • InputHandler interprets keyboard input events and generates control events
    • EventHandler processes control events and updates the game state (Pong class) accordingly

Input handling and Control Events (pt. 2)

Important Remark

  • Notice the time passing event corresponds to no user input
  • This complicates the design
    • conceptually, it implies that the system evolves even in absence of user inputs
    • practically, it implies that the system must be able to generate control events internally
  • At the modelling level, this means one more abstraction is needed
    • the control loop is actually the abstraction that takes care of time passing in the game

More design choices

  • To keep our design proposal simple, we:
    • consider the time passing event as a special kind of input
    • … not really provided by the user, but by the control itself
    • so, the InputHandler is also in charge of generating time passing events

Input handling and Control Events (pt. 3)

Important Remark

  • In the general case, Paddles are moved by players, via the keyboard
    • when players are distributed, the keyboards are different

      • so key bindings can be the same for all players (but still customisable)
    • when players are local, there’s only one keyboard

      • so key bindings must be different for each player

More design choices

  • Useful additional abstractions:
    • PlayerAction enumerates all possible actions a player can perform (on a paddle)
      • e.g. move paddle in a given Direction, stop paddle, quit the game
    • ActionMap, associating key codes to PlayerActions

Design Proposal (pt. 1)

left 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

  • each player is associated to a Paddle and to an ActionMap to govern that Paddle
  • an ActionMap is a dictionary mapping key codes to PlayerActions
    • e.g. pygame.K_UP $\rightarrow$ MOVE_UP, pygame.K_DOWN $\rightarrow$ MOVE_DOWN for right paddle (arrow keys)
    • e.g. pygame.K_w $\rightarrow$ MOVE_UP, pygame.K_s $\rightarrow$ MOVE_DOWN for left paddle (WASD keys)
  • PlayerActions are one particular sort of ControlEvents that may occur in the game
    • e.g. GAME_START, PADDLE_MOVE, TIME_ELAPSED, etc.
  • ControlEvents are custom PyGame events which animate the game
    • each event instance is parametric (i.e. may carry additional data)
      • e.g. PADDLE_MOVE carries the information about which Paddle is moving and where it is moving

Design proposal (pt. 2)

Example of Keyboard Input Processing

Design proposal (pt. 3)

Example of Time Elapsed Processing

Design proposal (pt. 4)

package “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

About outputs

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:

  • the output is just a rendering of the game state on the screen
  • this is possible because we separated model, and control, so now it’s time to separate the view
    • essentially, we’re following the MVC pattern
    • where model $\approx$ Pong, control $\approx$ EventHandler + InputHandler
  • if the rendering is updated frequently enough, the user perceives the game as animated
    • the “frequently enough” is the frame rate of the game (30-60 fps is common)

Design Proposal

@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)
  • notice the render() method

Pong

Wiring it all together

top 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()

Pong

Lancing the game

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

GitHub repository

  • minimal launch with poetry run python -m dpongpy --mode local

Towards Distributed Pong (pt. 1)

(only DS related aspects are discussed here)

  1. Use case collection:
    • where are the users?
      • sitting in front of they own computer, connected to the internet or LAN
      • we want users to be able to play togther from different locations
    • when and how frequently do they interact with the system?
      • sporadically they may start a game, but when that happens, interactions among users are very frequent
    • how do they interact with the system? which devices are they using?
      • pressing keyboard keys on one computer may impact what is displayed on another
    • does the system need to store user’s data? which? where?
      • no data needs to be stored, but a lot of information needs to be exchanged among users during the game
        • this may change if leaderboards are introduced
    • most likely, there will be multiple roles
      • just players
      • possibly spectators, i.e. players providing no input but getting the whole visual feedback

Towards Distributed Pong (pt. 1)


  1. Requirements analysis:
    • how to synchronize and coordinate inputs coming from different players?
      • possibly, some infrastructural component is needed, behind the scenes, to do this
    • will the system need to scale?
      • not really scale, but it must be able to support players joining and leaving the game at any time
    • how to handle faults? how will it recover?
      • what if a player goes offline during the game?
        1. e.g. the game pauses until the player reconnects
        2. e.g. the ball position resets to centre and the corresponding paddle is removed
        3. e.g. the corresponding paddle is frozen in place
      • what if the aforementioned infrastructural component becomes unreachable?
        1. e.g. the game pauses until the component is reachable again
    • acceptance criteria will for all such additional requirements/constraints
      • latency must be low enough to allow for a smooth game experience
        • e.g. avoid lag in the ball/paddle movements

Towards Distributed Pong (pt. 1)


  1. Design:
    • are there infrastructural components that need to be introduced? how many?
    • how do components distribute over the network? where?

In other words, how is the infrastructure of the system organized?

About the Distributed Pong Infrastructure (pt. 1)

No infrastructure (local)

Let’s focus on information flow:

No infrastructure for Pong

About the Distributed Pong Infrastructure (pt. 2)

Centralized infrastructure

Let’s suppose a central server is coordinating the game:

Centralized infrastructure for Pong

About the Distributed Pong Infrastructure (pt. 3)

Brokered infrastructure

Let’s suppose a central server is coordinating the game, but a broker is used to relay messages:

Brokered infrastructure for Pong

About the Distributed Pong Infrastructure (pt. 4)

Replicated infrastructure

Like the centralised one, but the server is replicated and a consensus protocol is used to keep replicas in sync:

Replicated infrastructure for Pong

About the Distributed Pong Infrastructure (pt. 5)

What to choose?

  1. Local infrastructure implies no distribution for players
  2. Centralized infrastructure implies a single point of failure (the server)
    • plus it complicates deploy:
      1. who’s in charge of starting the server?
        • e.g., one player (makes the start-up procedure more complex)
        • e.g., hosted online (makes access control more critical, adds deploy and maintenance costs)
      2. where should the server be located?
  3. Brokered infrastructure implies two single points of failure (the broker, the server)
    • essentially, it has all the drawbacks of the centralized infrastructure, plus the complexity of the broker
      1. where to deploy the broker?
      2. potentially higher latency, due to the additional hop for message propagation
    • may temporally decouple the server from the clients, which is a non-goal in our case
  4. Replicated infrastructure is overkill for a simple online game
    • for video games, it’s better to prioritize availability over consistency
    • no data storage $\implies$ no strong need for consistency

Towards Distributed Pong (pt. 2)


  1. Design:
    • are there infrastructural components that need to be introduced? how many?
      • e.g., one server to coordinate $N$ clients (one for each player)
    • how do components distribute over the network? where?
      • e.g., centralised server on the cloud, clients on all users’ computers
      • e.g., centralised server on one user’s computer, clients on all other users’ computers
    • how do domain entities map to infrastructural components?
      • e.g., PongGame entity updated by the server and replicated on all clients, in a master-slave fashion
      • e.g., one PongView entity per client, to render the game state on the local screen
      • e.g., one InputHandler entity per client, to grasp local inputs and send them to the server
      • e.g., one EventHandler entity on the server, to receive remote inputs and update the PongGame entity accordingly
    • how do components communicate? what? which interaction patterns do they enact?
      • e.g., request-response? publish-subscribe?
    • do components need to store data? what data? where? when? how many copies?
      • e.g., no real need for data storage, but the overall state of the game must be replicated on all clients ($N+1$ copies)
        • replication must be as quick/frequent as possible to avoid inconsistencies
        • prioritize availability over consistency
    • how do components find each other?
      • e.g. clients are assumed to know the IP of the central server
    • how do components recognize each other?
      • e.g. mutual trust is assumed, no need for authentication

Towards Distributed Pong (pt. 3)


  1. Implementation:
    • which network protocols to use?
      • e.g. UDP: good for low-latency, real-time applications (let’s go with this one!)
        • no big deal for package loss in a video game, yet duplication, unordered dispatch, and delays may give us headaches
      • e.g. TCP: good for reliability but may introduce latencies
        • may complicate the design of the central server, which will have to manage several simultaneous connections
    • how should in-transit data be represented?
      • e.g. any binary one would be better, to avoid latency bandwidth waste
        • we’ll go with JSON, for didactic purposes, then switch to BSON
    • no need for storage capabilities, so no need for a database
    • trust-based communication $\implies$ no need for authentication
    • trust-based authorization each client should only be able to command one and only one paddle
      • first-come-first-served policy for paddle assignment

Distributed Pong Architecture

Components view

Components diagram of the Distributed Pong architecture or Components diagram of the Distributed Pong architecture (coordinator hosted by some user)

  • Coordinator is the central server, coordinating the game

    • it runs the game loop and updates the game state, without listening to the keyboard, nor rendering the game
    • it receives inputs from the clients
    • it periodically sends the updated game state to the clients
  • Terminal is the client’s end point, visualising the game

    • it listens to the keyboard and sends inputs to the coordinator
    • it receives the game state from the coordinator and renders it on the screen
  • Event-Based Architecture:

    • the coordinator publishes the “game state update” events and consumes “input” events
    • terminals consume the “game state update” events and publish “input” events

Distributed Pong Architecture

Behavioural view

State diagrams of the Coordinator and Terminal nodes


  • 2 sorts of processes
    • one for the coordinator
    • one for the terminals
  • 2 sorts of communication channels for 2 sorts of events:
    • one for the game state updates
    • one for the inputs
  • each process is carrying on several concurrent activities:
    • the coordinator:
      1. listen for incoming inputs from terminals
      2. execute the game loop and send back game state updates
    • the terminals:
      1. sends inputs to the coordinator
      2. listen for incoming game state updates from the coordinator
      3. render the game state on the screen

Distributed Pong Architecture

Interaction view

Sequence diagram showing the interaction among the Coordinator and Terminal nodes


  • this is stressing when messages are exchanged, and by whom
  • the coordinator shall send state-update messages to all terminals once every $1/FPS$ seconds
  • the terminals shall send input messages to the coordinator whenever a key is pressed or released

Open Design Issues

Aren’t we missing something?

  1. How should players join the game?

  2. How should players leave the game?

Joining/Leaving Protocols (a proposal)

  1. Assumptions

    • the coordinator is started before any terminal
    • the coordinator is reachable by all terminals
  2. Joining:

    1. players start terminals by choosing their paddle (by side)
      • first-come-first-served policy
    2. terminals send a PLAYER_JOIN message to the coordinator upon starting
    3. the coordinator simply registers the new paddle, and resets the ball position
    4. the coordinator starts accepting inputs from the new paddle/terminal
      • and updates the game state accordingly
  3. Leaving (gracefully):

    1. players may press a key (e.g. ESC) to quit the game
    2. terminals send a PLAYER_LEAVE message to the coordinator upon quitting
      • only after sending the message, the terminal node can terminate
    3. upon receiving the message, the coordinator unregisters the paddle and resets the ball position
    4. the coordinator terminates if there are no more paddles
      • otherwise, it keeps operating as usual

Joining/Leaving Protocol (issues)

  1. what if the coordinator is not available when the terminal starts?

  2. what if a terminal crashes before sending the PLAYER_LEAVE message?

  3. how could the coordinator distinguish between a crashed terminal and one sending no inputs?

  4. what if the coordinator crashes while some terminals are still running?

  5. what if a terminal selects a side that is already taken?

  6. what if a terminal send inputs concerning the wrong paddle?

Joining/Leaving Protocol (solutions)

  1. what if the coordinator is not available when the terminal starts?

    • e.g., the terminal simply terminates
    • e.g., the terminal waits (up to a timeout, and no more than a maximum amount of retries) for the coordinator to become available
  2. what if a terminal crashes before sending the PLAYER_LEAVE message?

    • e.g., if UDP is used, some custom heart-beating mechanism may be needed
    • e.g., if TCP is used, the connection dies and the coordinator can notice the terminal’s crash
  3. how could the coordinator distinguish between a crashed terminal and one sending no inputs?

    • e.g. via timeouts on the coordinator side
  4. what if the coordinator crashes while some terminals are still running?

    • e.g. via timeouts on the terminal side
  5. what if a terminal selects a side that is already taken?

    • e.g. the coordinator silently ignores the PLAYER_JOIN message
    • e.g. the coordinator rejects the PLAYER_JOIN message and the terminal terminates
      • implies changing the joining protocol to a request-response one
  6. what if a terminal send inputs concerning the wrong paddle?

    • e.g. the coordinator silently ignores the inputs
      • implies the coordinator is keeping track of which paddle is associated with which terminal
    • e.g. the coordinator kicks out the misbehaving terminal
      • same implication as above
      • implies changing the leaving protocol in such a way it can be initiated by the coordinator

Distributed Pong Analysis (pt. 1)

Let’s focus on the currently available implementation:
https://github.com/unibo-fc-isi-ds/dpongpy

Project structure

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)

TODO

Distributed Pong Analysis (pt. 2)

W.r.t. the previous corner cases, in which situations are we?

How to know?

  1. what if the coordinator is not available when the terminal starts?

    • Start the terminal when no coordinator is running, what happens?
  2. what if a terminal crashes before sending the PLAYER_LEAVE message?

    • Start the coordinator and then 2 terminals for 2 different paddles. Kill one terminal. What happens?
  3. what if the coordinator crashes while some terminals are still running?

    • Start the coordinator and then 1 terminal. Kill the terminal. What happens?
  4. what if a terminal selects a side that is already taken?

    • Start the coordinator and then 2 terminals for the same paddle. What happens?
  5. what if a terminal send inputs concerning the wrong paddle?

    • Inject a bug into the terminal to force sending events for the same paddle. Start the coordinator and then 2 terminals. What happens?
      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
              ...
      

Distributed Pong Analysis (pt. 2)

Are we prioritizing availability or consistency with the current design/implementation?

How to know?

  1. Let’s try to simulate a network partition:

    • we’re using UDP, so message drops are possible already, when the coordinator and the host are on different machines
    • to make it network partitioning more evident, let’s force a higher package drop rate
      • set the 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$
        • meaning that roughly the $100p$% of the packages will be dropped (20% in our case)
      • … then launch the coordinator and the terminal
  2. is the gameplay fluid or laggy?

    • fluid $\approx$ we’re prioritizing availability
    • laggy $\approx$ we’re prioritizing consistency

Exercise: Available Distributed Pong

  • 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

    1. the terminal’s game-loop should never block
      • in particular, it should not block while waiting for the coodinator’s updates
    2. rather, the terminal should continue updating the local game state according to the local inputs, and render it as usual
    3. upon receiving a new update from the coordinator, the terminal should overwrite the local game state with the remote one
  • Deadline: two weeks from today

  • Incentive: +1 point on the final grade (if solution is satisfying)

  • Submission:

    1. fork the dpongpy repository
    2. create a new branch named exercise-lab1
    3. commit your changes to the new branch
    4. push the branch to your fork & create a pull request to the original repository, entitled [A.Y. 2024/2025 Surname, Name] Exercise: Available Distributed Pong
      • in the pull request, describe your solution, motivate your choices, and explain how to test it

Lecture is Over


Compiled on: 2025-06-30 — printable version

back to ToC