Jan 2025 – Apr 2025
SWELast 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
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 over players () is a sequence of one drawing phase and guessing rounds. During the drawing phase, every player is assigned a private word from a shared word bank, has a fixed window of to produce a drawing , 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 submitted at time into the round is
with and the round’s total available time. The inside the bracket is deliberate: at the player earns the full , at 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 (P1), score updates must fan out to every client within of a round ending (P2), a first-time player must be able to complete a full game with no external help on of attempts (U1), and a developer with no prior exposure to the codebase must be able to add a new word in under (M1), tweak the scoring rule in under by editing one Java class (M2), and add a new drawing tool in under (M3).
Approach
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 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 . 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 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 rounds loop.
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 (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 above (FR7.1, of calculator validation), and the final leaderboard appears within of the last guess (FR9).
| Modifiability scenario | Target | Measured | Outcome |
|---|---|---|---|
| M1, Add a new word | ✅ | ||
| M2, Change scoring rule | ✅ | ||
| M3, Add a new drawing tool | ✅ |
| Usability scenario | Target | Measured | Outcome |
|---|---|---|---|
| U1, First-time player finishes a game unassisted | (39 / 42 points of interest, 3 external testers) | ✅ | |
| U2, Button purpose identified within 3 s | ✅ |
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.



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, rounds) start to feel slow once . 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.