TFM Project Architecture
Directory Structure
src/ ├── server/ # Backend game logic (Node.js) ├── client/ # Frontend (Vue.js) ├── common/ # Shared types/enums/models between server & client ├── cards/ # Breakthrough card overrides (thin layer) ├── locales/ # i18n translation files (.json) ├── styles/ # CSS/Less styles └── tools/ # Utility scripts
Three-Layer Architecture
1. Common Layer (src/common/)
Shared TypeScript types, enums, and models. No game logic — only data definitions consumed by both server and client.
Key files:
| File/Dir | Purpose |
|---|---|
cards/CardName.ts | Enum of ALL card names (identifiers) |
cards/CardType.ts | Enum: EVENT, ACTIVE, AUTOMATED, PRELUDE, CORPORATION, CEO, etc. |
cards/Tag.ts | Enum: BUILDING, SPACE, SCIENCE, POWER, EARTH, etc. |
cards/render/ | Render type definitions (CardRenderItemType, Size, Types) |
Resource.ts | Enum: MEGACREDITS, STEEL, TITANIUM, PLANTS, ENERGY, HEAT |
CardResource.ts | Card-specific resources (microbe, animal, floater, etc.) |
models/ | Serialized models sent to client (GameModel, PlayerModel, CardModel) |
Phase.ts | Game phases |
constants.ts | Global game constants |
Types.ts | Core ID types (PlayerId, GameId, etc.) |
2. Server Layer (src/server/)
All game logic runs here. The server is authoritative.
Core Files
| File | Purpose |
|---|---|
Game.ts | Main game class (~2500 lines). Manages game state, phases, turns, global parameters, tile placement, draft, generation flow. |
Player.ts | Player class (~2100 lines). Manages player resources, production, card playing, action handling, payments. |
IGame.ts | Game interface definition. |
IPlayer.ts | Player interface definition. |
GameSetup.ts | Game initialization logic. |
server.ts | HTTP server entry point. |
Cards (src/server/cards/)
The largest subsystem. Each card is a separate .ts file organized by expansion module:
cards/ ├── base/ # Base game cards ├── corporation/ # Corporation base classes (CorporationCard, ICorporationCard) ├── commission/ # Custom/commissioned cards (prefix 🌸) ├── promo/ # Promo cards ├── colonies/ # Colonies expansion ├── venusNext/ # Venus Next expansion ├── moon/ # Moon expansion ├── pathfinders/ # Pathfinders expansion ├── turmoil/ # Turmoil expansion ├── underworld/ # Underworld expansion ├── prelude/ # Prelude expansion ├── prelude2/ # Prelude 2 ├── eros/ # Eros expansion ├── community/ # Community cards ├── breakthrough/ # Breakthrough card variants ├── ceos/ # CEO cards ├── starwars/ # Star Wars cards ├── render/ # Card rendering system (CardRenderer, CardRenderItem, etc.) ├── requirements/ # Card requirement logic ├── AllManifests.ts # Aggregates all module manifests ├── Card.ts # Abstract base Card class ├── ICard.ts # Card interface with all callbacks ├── IProjectCard.ts # Project card interface ├── ActionCard.ts # Base class for cards with data-driven `action` ├── Deck.ts # Deck management (draw, discard, shuffle) └── ModuleManifest.ts # Module manifest type definition
Card Type Hierarchy
ICard (interface) ├── Card (abstract class) — most cards extend this │ ├── implements IProjectCard — for project cards (ACTIVE/AUTOMATED/EVENT) │ └── CorporationCard (abstract) — for corp cards │ └── ActiveCorporationCard — corp cards with action() └── ActionCard — project cards with data-driven action behavior
Other Server Subsystems
| Directory | Purpose |
|---|---|
boards/ | Board logic (Mars board, spaces, tile placement) |
behavior/ | Behavior system — declarative card effects via JSON-like Behavior type |
deferredActions/ | Action queue system (DrawCards, PlaceOceanTile, GainResources, etc.) |
turmoil/ | Turmoil political system (parties, delegates, global events) |
colonies/ | Colony system |
moon/ | Moon expansion logic |
awards/ & milestones/ | End-game scoring |
routes/ | HTTP API handlers |
database/ | Game persistence |
inputs/ | Player input types (SelectCard, SelectSpace, OrOptions, etc.) |
models/ | Server → client model serialization |
3. Client Layer (src/client/)
Vue.js frontend. Receives GameModel/PlayerModel from server and renders the UI.
client/ ├── components/ # Vue components (.vue files) ├── cards/ # Client-side card rendering helpers ├── utils/ # Client utilities ├── directives/ # Vue directives ├── mixins/ # Vue mixins ├── plugins/ # Vue plugins └── turmoil/ # Turmoil-specific client code
Card Renderer System
Cards define their visual appearance using CardRenderer.builder() — a builder-pattern DSL in src/server/cards/render/CardRenderer.ts.
Builder API Reference
The builder (Builder<T>) provides chainable methods:
Resources:
- •
megacredits(n),steel(n),titanium(n),plants(n),energy(n),heat(n) - •
cards(n, options?)— draw cards icon - •
tr(n)— terraform rating
Global Parameters:
- •
temperature(n),oxygen(n),oceans(n),venus(n)
Tiles:
- •
city(options?),greenery(options?),emptyTile(type?),specialTile(options?)
Tags:
- •
tag(Tag, options?),wild(n),noTags(),emptyTag(),diverseTag()
Card Resources:
- •
resource(CardResource, options?)— e.g. microbe, animal, floater
Production Box:
- •
production((pb) => { pb.energy(1); pb.steel(2); })— wraps items in production border
Layout:
- •
.br— line break - •
.nbsp— non-breaking space - •
vSpace(size?)— vertical space - •
text(str, size?, uppercase?, bold?),plainText(str),vpText(str) - •
plate(str)— plate text
Effect/Action (for ACTIVE/CORP cards):
- •
effect(description, (eb) => { eb.cause.startEffect.result })— triggered effect - •
action(description, (eb) => { eb.cost.startAction.result })— player action - •
corpBox('effect'|'action', (ce) => { ... })— corporation box
Symbols:
- •
startEffect— colon separator (:) for effects, starts new row - •
startAction— arrow separator (->) for actions, starts new row - •
or(),plus(),minus(),slash(),asterix()
Effect/Action Structure
Each effect() or action() callback must produce exactly 3 rows:
- •Cause/Cost — what triggers or what you pay
- •Delimiter —
startEffect(:) orstartAction(->) - •Result — what you get
If no cause, start with empty().
Examples
Simple automated card:
CardRenderer.builder((b) => {
b.production((pb) => pb.energy(1).steel(2));
});
Active card with effect:
CardRenderer.builder((b) => {
b.effect('When you play a building tag, gain 2 M€ discount.', (eb) => {
eb.tag(Tag.BUILDING).startEffect.megacredits(-2);
});
});
Corporation card:
CardRenderer.builder((b) => {
b.megacredits(55).cards(1, {secondaryTag: Tag.SCIENCE});
b.corpBox('effect', (ce) => {
ce.vSpace(Size.LARGE);
ce.effect('description', (eb) => {
eb.cards(1).startEffect.megacredits(1);
});
});
});
Behavior System
The Behavior type (src/server/behavior/Behavior.ts) allows declarative card effects. Instead of writing imperative play() code, define a behavior object on the card:
behavior: {
production: {energy: 1, steel: 2}, // gain production
stock: {megacredits: 5}, // gain resources
global: {temperature: 1}, // raise global param
drawCard: 2, // draw cards
tr: 1, // gain TR
city: {}, // place a city
greenery: {}, // place a greenery
ocean: {}, // place an ocean
}
The BehaviorExecutor automatically handles canPlay and play for behavior-defined effects. For custom logic, override bespokeCanPlay() and bespokePlay().
Key Callbacks on ICard
Cards interact with the game via callback methods defined in ICard:
| Callback | When Called |
|---|---|
play(player) | When card is played |
onCardPlayed(player, card) | When this card's owner plays another card |
onCardPlayedByAnyPlayer(owner, card, activePlayer) | When any player plays a card (for corp cards: onCardPlayedForCorps) |
onTilePlaced(cardOwner, activePlayer, space) | When any tile is placed |
onResourceAdded(player, card, count) | When resources added to a card |
onProductionGain(player, resource, amount) | When production changes |
onGlobalParameterIncrease(player, param, steps) | When a global param increases |
onColonyAddedByAnyPlayer(cardOwner, colonyOwner) | When a colony is built |
canAct(player) / action(player) | For ACTIVE cards with actions |
getCardDiscount(player, card) | Calculate M€ discount for playing a card |
getVictoryPoints(player) | End-game VP calculation |
Module Manifest System
Each expansion module defines a ModuleManifest mapping CardName → card factory:
export const COMMISSION_CARD_MANIFEST = new ModuleManifest({
module: 'commission',
corporationCards: { [CardName.XXX]: {Factory: XxxClass} },
projectCards: { [CardName.YYY]: {Factory: YyyClass} },
preludeCards: {},
globalEvents: {},
});
All manifests are aggregated in src/server/cards/AllManifests.ts.