The Thing Card

# Stay Away: A Multiplayer Card Game

Table of Contents

Summary:

For our final Software Engineering course, my team of six took on a pretty significant challenge: building a full-stack, real-time web version of “Stay Away”, a hidden-identity card game heavily inspired by John Carpenter’s “The Thing.”

Tech Stack

We built this as a full-stack web application, maintaining a very clear separation between the client and server.

  • Backend: A Python API built on FastAPI. This was the brain, handling all game logic and managing the entire game state.
  • Database & ORM: We went with PonyORM to model our game entities (players, cards, etc.) and used a SQLite database for persistence.
  • Frontend: The client side is a React app, built with Vite. We used plain JavaScript for this part.
  • Real-time Communication: For the live chat feature, we implemented WebSockets to allow players to communicate instantly.

Technical Challenges

An Event-Driven Approach to Multi-Player Actions

A central architectural challenge was the event-driven approach to multi-player actions. Frequently, an action from Player A requires an immediate reaction from Player B.

For instance, consider the “Lanzallamas” card:

A card named Lanzallamas

The game can’t simply proceed. The backend must interrupt the main flow, switch the game’s state to defense, and assign Player B as the current_player.

We tackled this by implementing an event-driven system relying on our Event model in the database. The flow works like this:

  1. Player A plays “Lanzallamas.”
  2. The CardPlayer class generates an incomplete Event record, setting is_completed=False.
  3. This transitions the game state to defense, where it waits for Player B.
  4. Once Player B responds, their action finds that specific incomplete event, resolves it (by setting is_completed=True), and the outcome is determined.

This database-backed event queue became the core mechanism for handling these multi-step, asynchronous interactions.

Managing Dynamic Turn Progression and Player Adjacency

We also had to manage dynamic turn progression and player adjacency. These concepts just weren’t static. For instance:

  • The “Vigila tus espaldas” card can reverse the direction of play instantly.

    A card named Vigila tus espaldas
  • The “Puerta Atrancada” card might block interactions between two players who are normally adjacent.

    A card named Puerta Atrancada

This meant we couldn’t depend on a simple, fixed list for turn order or for finding valid targets.

  • Turn Progression: The turn_handler.py module always checks the game’s game_order (which could be right or left) before it calculates the next player’s position.
  • Player Adjacency: In a similar way, our validation layer for any card action checks for Obstacle entities (using lacosa/game/utils/obstacles.py) before it permits a player to target an adjacent one.

This approach ensured that the turn flow and card effects always respected the game board’s current, dynamic layout.

Translating Database Events into a Human-Readable Game Log

Another task was translating database events into a game log that people could actually read. The backend was fine tracking the game flow with structured Event objects, but the frontend needed a simple log.

Database Event (Backend):

{
"event_type": "attack",
"source_id": 8,
"target_id": 12,
"card_id": 22,
"is_successful": false,
"response_card_id": 45
}

Game Log (Frontend):

Player A attacked Player B with “Lanzallamas.” Player B defended with “No, gracias.”

This translation of raw database entries into natural language was a significant user experience problem.

We built a dedicated serialization module, event_serializer.py, to solve it. This module essentially works as a translator. It holds a dictionary that maps each EventTypes enum to a specific formatting function. This design choice gave us a clean separation between the backend’s internal state and the frontend’s presentation needs, letting us generate a clear narrative log for the players.

Designing an Extensible Card Effect System

Finally, we had to design an extensible card effect system. The game features a diverse deck, and every card has its own unique logic.

A giant if/elif/else block to process every single card was not an option. That would have been a maintenance nightmare:

  • Brittle and prone to bugs.
  • A pain to test.
  • Extremely hard to expand with new cards.

We required a flexible architecture, one that would allow us to add new cards and mechanics without breaking everything.

When any card is played, the generic execute_card_effect function just looks up the card’s name in this dictionary and dynamically calls the correct function.

This approach decoupled what card was played from how its logic is run. This made the whole system highly extensible. To add a new card like “Análisis,” all we had to do was write an apply_analysis_effect function and then add one line to that dictionary. No changes to the core card-playing logic were needed at all.

A card named Análisis

Project History

This all started as our final assignment for Software Engineering. As a six-person team, we split into frontend and backend sub-teams.

We ran the project using an Agile methodology with a two-week sprint cycle, complete with sprint planning, poker planning for estimations, and mandatory PR reviews. Adding tests wasn’t optional.

I took a lead role on the backend side, where I was responsible for:

  • Designing the core architecture.
  • Building the crucial state machine to manage the game’s turn-based flow (draw, action, defense, trade).
  • Implementing the logic for the most complex card effects.

To improve our team’s workflow, I also set up the CI pipeline using GitHub Actions to automate linting and testing, which helped us maintain good code standards.

Because I had some prior frontend experience, I also became the communication bridge between the backend and frontend teams. This was key for debugging tricky integration issues and making sure neither team got blocked, which helped us pull it all together for the final demo.

Game Overview

The game is designed for 4 to 12 players. At the beginning, one player is secretly “The Thing,” and everyone else is “Human.”

  • The Thing’s goal: Infect other players.
  • The Human’s goal: Identify and eliminate The Thing with a “Lanzallamas” card.

The gameplay is all about drawing, playing, and trading cards to deduce who is who.

Strategic depth comes from a diverse deck of cards, each with a specific job:

  • Contagion Cards: The “Infeccion” card. This is the heart of the game’s paranoia.

  • Action Cards: These directly affect other players. Everything from the game-ending “Lanzallamas” to info-gathering cards like “Sospecha” (peek at a card).

  • Defense Cards: These are played out of turn and are the only way to save yourself from a “Lanzallamas” or refuse a suspicious trade.

  • Panic Cards: These trigger immediately when drawn and just cause chaos, like forcing players to reveal their hands.

Future Plans

While it began as a university project, I do have plans to continue its development. My main goals are to:

  1. Get it properly deployed so people can play it live.
  2. Clean up the repositories and refactor a few modules for better maintainability.

Where to try it?

Deployment is still a work in progress, but I’ll update this post when it’s fully functional.

For now, you can check out the code for the project in the following repositories:

My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


More Posts

Comments