Back to all projects

Jan 2025 – Apr 2025

SWE

Last edited

DrawGuess: Multiplayer Pictionary Reinvented

DrawGuess is a turn-based multiplayer Pictionary-style mobile game for Android. Players are first assigned as drawers (each given a word to draw on their screen within a time limit), then cycle through a guessing phase where everyone tries to identify other players' drawings. Correct guesses score points; the round ends once every player has guessed every drawing except their own.

The architecture combines Model-View-Controller for screen organisation, Client-Server for the multiplayer backend (Firebase Firestore + Storage and Socket.IO for real-time events), and Entity-Component-System for the drawing canvas and dynamic game-state objects. Singleton and Factory patterns handle global session management and screen instantiation.

Built for NTNU's TDT4240 Software Architecture course, with modifiability as the primary quality attribute and usability and performance as secondary. Architectural decisions were validated against functional and quality requirements through a structured test report.

Affiliation

NTNU

Partners

Report

  • Implementation report

Keywords

  • Software Architecture
  • Design Patterns
  • Entity-Component-System
  • Client-Server Architecture
  • State Synchronization
  • Modifiability
  • Java
  • LibGDX
  • Android SDK
  • Firebase Firestore
  • Firebase Storage
  • Socket.IO
  • Render

Deepdive

Introduction

DrawGuess is a multiplayer Pictionary-style Android game built in spring 2025 for NTNU’s TDT4240 Software Architecture course. The twist on the genre is structural: instead of one drawer per round with everyone else watching live, every player draws their own word simultaneously during a fixed drawing window, and the resulting drawings are then queued and guessed in a real-time guessing phase. The project’s primary quality attribute was modifiability, the team wanted a codebase where a developer could add a word, change a scoring rule, or swap in a new drawing tool without touching unrelated game logic, with usability and performance as secondary attributes. The implementation pairs a LibGDX client with a Node.js/Socket.IO server on Render and Firebase (Firestore + Storage) for persisted state and binary asset transfer.

Problem Definition

A game session GG over NN players (2N82 \leq N \leq 8) is a sequence of one drawing phase and N1N-1 guessing rounds. During the drawing phase, every player pip_i is assigned a private word wiw_i from a shared word bank, has a fixed window of Tdraw=60sT_{\text{draw}} = 60\,\text{s} to produce a drawing did_i, and submits it via a “Done” tap. During the guessing phase, each player sees all drawings except their own and submits exactly one guess per drawing, and the score awarded for a correct guess on djd_j submitted at time tt into the round is

S(t)  =  {Smax ⁣(1,log(t+1)log(Tmax+1))0tTmax,0otherwise,S(t) \;=\; \begin{cases} \left\lfloor S_{\max} \!\left(1, \dfrac{\log(t+1)}{\log(T_{\max}+1)}\right) \right\rfloor & 0 \leq t \leq T_{\max}, \\[6pt] 0 & \text{otherwise,} \end{cases}

with Smax=500S_{\max} = 500 and TmaxT_{\max} the round’s total available time. The log10\log_{10} inside the bracket is deliberate: at t=0t = 0 the player earns the full SmaxS_{\max}, at t=Tmaxt = T_{\max} the term inside the floor goes to zero, and the curvature in between drops points fast at first and then more slowly, which gives players a strong incentive to answer quickly while keeping late guesses worth submitting. The system must also satisfy a small set of quantitative quality constraints: any join must surface the player in the lobby within 3s3\,\text{s} (P1), score updates must fan out to every client within 500ms500\,\text{ms} of a round ending (P2), a first-time player must be able to complete a full game with no external help on 90%\geq 90\,\% of attempts (U1), and a developer with no prior exposure to the codebase must be able to add a new word in under 5min5\,\text{min} (M1), tweak the scoring rule in under 5min5\,\text{min} by editing one Java class (M2), and add a new drawing tool in under 30min30\,\text{min} (M3).

Approach

Architecture of DrawGuess: a LibGDX-based Android client organised by MVC with a custom ECS scoped to the canvas, a Node.js/Socket.IO game server hosted on Render, and Firebase Firestore plus Storage for persisted state and submitted drawings. Socket.IO carries lobby presence and round coordination, Firestore handles real-time game state, and Firebase Storage handles uploaded PNGs.
System architecture. The Android client follows MVC at the top level, with a small ECS scoped to the canvas; the Node.js server hosted on Render handles session orchestration, scoring, and lobby coordination over Socket.IO; Firebase provides Firestore for real-time game state plus Storage for the submitted drawings.

The system decomposes into a client, a game server, and a managed data tier. Each subsystem is internally structured to keep the three modifiability scenarios cheap: words live behind a single Java file, the scoring rule lives behind a single class, and drawing tools live behind a single factory plus their own components.

Client: MVC over LibGDX with an ECS Canvas

The Android client is a LibGDX application structured around MVC. The View layer is a set of LibGDX Screen subclasses (HomeMenuScreen, LobbyScreen, DrawingScreen, GuessingScreen, LeaderboardScreen) coordinated by a GameStateManager. Controllers (GameController, PlayerController, DrawingController) sit between the screens and the model, and a State pattern over the controller layer encodes the Waiting → Drawing → Guessing → Leaderboard progression so that screen transitions and the events each screen responds to are tied to a single explicit state variable rather than scattered across boolean flags. The Model includes a GameManager Singleton (the central access point for the current GameSession, playerId, and active GameController), a GameSession aggregate, Player/Score/Word data classes, and the WordBank referenced by M1.

The canvas is the one place where MVC alone doesn’t carry well, so it gets its own pattern. A lightweight, hand-rolled Entity-Component-System sits inside the model: each drawing tool is an Entity composed of pure-data components (ToolComponent carrying a ToolType and a stroke size, ColorComponent carrying a Color), constructed by a ToolFactory, and rendered by a DrawingSystem that applies strokes to the canvas based on the tool’s components rather than its class. Adding a new tool, eraser, fill, or anything else, is then just two new components or an enum value plus a factory method, with no change to DrawingSystem itself. This is the architectural shape that makes the M3 (30-minute) budget for new tools achievable, and in the M3 verification test it was actually achieved in 27min27\,\text{min} end-to-end.

Server: Node.js + Socket.IO on Render

The game server is a Node.js process hosted on Render that accepts Socket.IO connections from clients and serves four kinds of traffic: lobby presence (registerLobby, unregisterLobby), session lifecycle (createGame, emitStartGame), round orchestration (submitDrawing, submitGuess, phaseTransition), and leaderboard fan-out. Socket.IO sits on top of WebSockets and falls back to HTTP long-polling on networks that block raw WS, which buys robustness on flaky mobile connections at the cost of a small abstraction-layer latency relative to raw ws. The cold-start behaviour of Render, idle dynos can take several seconds to spin back up, is mitigated with a periodic keep-alive ping; this is not free, but it keeps the P1 join-time budget achievable during multiplayer testing.

The scoring formula above lives entirely in one Java class on the server, intentionally so. The M2 quality scenario asks for a developer with no prior exposure to the codebase to change the rule in under five minutes; in the M2 verification test the actual measured time was 3.4min3.4\,\text{min}. The cost of this discipline is that scoring is server-authoritative, clients display points but never compute them, which is also why the P2 budget of 500ms500\,\text{ms} from “round ends” to “all clients see updated scores” is realistic in the first place: the calculation never crosses a network boundary, only its result does.

Data Tier: Firestore for State, Storage for Pixels

Firestore holds everything that is queried in real time: per-session player rosters, round phase, current word assignments, accumulated leaderboard scores, and the matchmaking PINs that let a “Join Game” enter the right session. Real-time listeners on the client surface Firestore writes to the View layer within a few hundred milliseconds, which is what makes the P1 (3-second join) and P2 (500 ms score update) budgets tractable without writing any custom synchronisation. Drawings are intentionally not stored in Firestore: when a player taps “Done” the canvas is exported as a PNG, uploaded to Firebase Storage, and only the resulting download URL is written back to Firestore. This keeps the document size in Firestore bounded and exploits Storage’s CDN for the actual binary fan-out to the other players in the guessing phase.

Patterns Above the Architecture

Three smaller patterns sit on top of this structure to keep the modifiability story coherent. The Singleton on GameManager removes the need to thread the active session through controller constructors. The Factory pattern shows up three times, ToolFactory (ECS entities), ToolButtonFactory (Scene2D UI buttons), and PlayerFactory (host vs. participant), and each factory’s job is the same: separate “what kind of thing this is” from “what shape it has in memory” so that new variants can be added in one place. The State pattern in the controller layer keeps the phase machine explicit; in practice this is what makes the WaitingPlayers → Drawing → Guessing → Leaderboard sequence readable in the test report’s N1N-1 rounds loop.

Game flowchart: Start page → Game lobby → Drawing phase, which fans out to N parallel drawer slots and initialises a round counter x = 1. All drawers feed into the Guessing phase, which fans out to N guesser slots. After every round the system checks whether x equals N − 1: MISS routes back to the Guessing phase with x incremented, HIT routes to the Leaderboard, and the Leaderboard loops back to the Start page.
End-to-end game flow. Each player draws once in parallel during the drawing phase, then the guessing phase iterates N,1N, 1 times, once per drawing that any given player has to guess, with a round counter xx that increments on every MISS and exits to the leaderboard on the HIT branch when x=N1x = N-1.

Results

The functional surface was verified end-to-end through 17 black-box tests against the requirements specification, with the test executors timing the time-to-completion for each scenario. The headline results: a player can start a session in 30s30\,\text{s} (FR1), the drawing-then-guessing role assignment is enforced by the one-minute drawing timer (FR2), the canvas accepts touch gestures (FR3), drawings are visible to other players in the guessing phase (FR4), text guesses are validated and scored (FR5–FR7), the per-round scoring matches the closed-form expression in S(t)S(t) above (FR7.1, 60s60\,\text{s} of calculator validation), and the final leaderboard appears within 30s30\,\text{s} of the last guess (FR9).

Modifiability scenarioTargetMeasuredOutcome
M1, Add a new word5min\leq 5\,\text{min}2min2\,\text{min}
M2, Change scoring rule5min\leq 5\,\text{min}3.4min3.4\,\text{min}
M3, Add a new drawing tool30min\leq 30\,\text{min}27min27\,\text{min}
Usability scenarioTargetMeasuredOutcome
U1, First-time player finishes a game unassisted90%\geq 90\,\%93%93\,\% (39 / 42 points of interest, 3 external testers)
U2, Button purpose identified within 3 s90%\geq 90\,\%97%97\,\%

Three of the lower-priority functional requirements failed verification and are useful to call out by name rather than hide. FR11 (mid-game reconnect after a Wi-Fi drop) failed because no reconnect logic was wired through the Socket.IO client; the test executor toggled Wi-Fi mid-game and the session was lost. FR12 (maximum eight players per session) failed because the eight-player cap was not enforced on the server, a ninth emulator joined a session in the verification test. FR13 (notify all players when a new round begins) failed because no explicit “new round” toast was implemented; players who were paying attention noticed the phase change, but the spec asked for a notification and the spec was not met. FR15 (lobby chat), FR16 (accessibility mode), and FR17 (profanity filter) were scoped out during implementation and are honestly logged as unimplemented rather than dressed up.

DrawGuess home screen, with two crayon-styled buttons labelled START GAME and JOIN GAME over a hand-drawn background of doodles, crayons, and watercolour pans.
Home screen. Two entry points, create a session as host, or join an existing session via PIN, over the game’s hand-drawn brand language.
DrawGuess drawing phase: a clipboard-style canvas with the player's secret word 'apple' shown at the top, a 45-second countdown, a partial sketch of an apple, an eraser and pencil tool, and an eight-colour palette along the bottom edge.
Drawing phase. The drawer’s secret word and the remaining time sit above the canvas; the bottom strip carries the eraser, the size selector, and the eight-colour palette. The “DONE” button in the top left commits the PNG to Firebase Storage and ends the player’s drawing turn.
DrawGuess final leaderboard, styled as a wooden clipboard with the heading LEADERBOARD and two rows: player1 with 96 points and host with 14 points, plus a back arrow to return to the home menu.
Final leaderboard. The score column reflects the log\log-shaped points-per-second formula in S(t)S(t) accumulated across all N1N-1 guessing rounds; the top-left arrow returns the player to the home menu and closes the session.

Future Work

The most impactful next change is closing the three FR-failures, in roughly the order their severity bites real players. FR12, the eight-player cap, is a one-line guard on the server’s join handler and should land first; the requirement existed precisely because Socket.IO fan-out cost grows with the number of subscribers and the game’s UX assumptions (one guess per drawing, N1N-1 rounds) start to feel slow once N>8N > 8. FR11, reconnect, is a deeper change that needs the client to persist its sessionId and playerId across an app/network restart and the server to keep a brief reconciliation window after a socket drop instead of treating it as a leave; this is the single largest improvement available for the perceived robustness of the game on mobile networks. FR13, round-start notification, is cheap (a Scene2D toast wired into the State pattern’s transition into Guessing) and is mostly a discipline fix.

The architecture itself has two pieces that would benefit from a second pass. The MVC layering on top of LibGDX is the team’s primary self-criticism in the implementation report, because LibGDX’s Screen/Stage model already implies a view+controller fusion that MVC then duplicates; a reactive or observer-based approach over LibGDX scenes would remove a layer of abstraction without losing the modifiability story, since the ECS for the canvas, the Singleton on GameManager, and the WordBank/Scoring split are the parts that actually carry the M1/M2/M3 budgets. The Client-Server model with Firebase as the central server is the right choice for a turn-based game with bounded round counts, but the guessing phase has fully real-time semantics that would be cheaper to run peer-to-peer, since each guess only matters to the players in the same session and routing through Firebase adds a fan-out hop that is otherwise unnecessary; refactoring the FirebaseInterface abstraction to a NetworkInterface and implementing a P2P backend behind it would be a contained change that opens the door to a hybrid mode.

A further direction worth exploring is moving the WordBank from a compiled-in Java file to a Firestore-backed list of word collections, which preserves the M1 budget for developers (still one place to edit) and additionally lets players bring their own word lists into a session. The current implementation deliberately keeps it as compiled state to make M1’s verification time literally a code edit, but in a shipped product a runtime word source is the more interesting place for this to live.