Play 2-vs-2 and 3-vs-3: team mode
Team up with a friend, pool your properties, and win together — opolyx now has 2×2 and 3×3 team games.

In plain words
Some games are better with a partner. Team mode pits two sides against each other — 2-vs-2 in a four-player game or 3-vs-3 in a six-player one — so you and your friends play for one colour, not just for yourselves.
Being on the same side changes everything about how you play together. Your teammates are allies, not rivals, so the board opens up:
- Land on a teammate’s property and you pay nothing — rent is only ever charged to the other team.
- Pass cash and property between teammates freely — there’s no 50% rule, so you can outright gift a struggling partner what they need.
- If a teammate goes bankrupt, their whole estate goes to you, not the bank — a team is only out when every member is gone.
- You win as a team: last side standing, or the richest side when the round limit hits — and both teammates get the win reward (XP and a card).
Coordinate your trades, cover for each other, and squeeze the rival team off the board together. It’s the same opolyx you know — with a partner in your corner.
For the technically curious
Team mode is the Classic ruleset with one new per-player concept: a team. The engine grows a PlayerState.team field and a handful of selectors (sameTeam, teamOf, aliveTeams, teamNetWorth) that the rules read: computeRent exempts teammates, doProposeTrade skips the 50% rule between them, and elimination routes the estate to the lowest-slot living teammate instead of the bank. End conditions are evaluated per team — last team standing, or richest team at the round cap.
Three migrations land it. Postgres forbids using a new enum value in the same transaction that adds it, so 0028 commits the team game_mode on its own; 0029 then lets create_game accept it (with a full, even-roster check) and adds award_game_winners for the multi-winner reward; 0030 adds the pickable team column and its lobby RPC.
-- 0028_team_mode_enum.sql — its own migration (see above)
alter type game_mode add value 'team';
-- 0030_team_picker.sql — pickable side; NULL = slot-parity default (slot % 2)
alter table game_players
add column team smallint check (team is null or team in (0, 1));
-- SECURITY DEFINER lobby write (RLS has no user write on game_players):
-- a player sets their own seat; the creator may set any seat (to arrange bots).
create function set_player_team(p_game uuid, p_slot int, p_team smallint)
returns void language plpgsql security definer as $$ ... $$;No event_kind enum change was needed — team data rides optional payload fields on existing events (game_started.players[].team, player_eliminated.inheritedBySlot, game_finished.winnerTeam). Picked teams (not parity) are threaded into initializeGame; the daemon refuses to start a team game whose roster isn’t full (team_roster_not_full) or isn’t balanced (team_not_balanced). Engine 214 / daemon 82 tests green, and the fast-check termination property now drives 2×2 and 3×3 games to a finish.
