golfgame/server/RULES.md

30 KiB

6-Card Golf Rules

Single Source of Truth for all game rules, variants, and house rules. This document is the canonical reference - all implementations must match these specifications.

Document Structure

This document follows a vertical documentation structure:

  1. Rules - Human-readable game rules
  2. Implementation - Code references (file:line)
  3. Tests - Verification test references
  4. Edge Cases - Documented edge case behaviors

Part 1: Core Game Rules

Overview

Golf is a card game where players try to achieve the lowest score over multiple rounds ("holes"). The name comes from golf scoring - lower is better.

Players & Equipment

  • Players: 2-6 players
  • Deck: Standard 52-card deck (optionally with 2 Jokers)
  • Multiple decks: For 5+ players, use 2 decks
Implementation File
Deck creation game.py:89-104
Multi-deck support game.py:92-103
Tests File
Standard 52 cards test_game.py:501-504
Joker deck 54 cards test_game.py:506-509
Multi-deck test_game.py:516-519

Setup

  1. Dealer shuffles and deals 6 cards face-down to each player
  2. Players arrange cards in a 2 row x 3 column grid:
    [0] [1] [2]   <- Top row
    [3] [4] [5]   <- Bottom row
    
  3. Remaining cards form the draw pile (face-down)
  4. Top card of draw pile is flipped to start the discard pile
  5. Each player flips 2 of their cards face-up (configurable: 0, 1, or 2)
Implementation File
Deal 6 cards game.py:304-311
Start discard pile game.py:313-317
Initial flip phase game.py:326-352
Tests File
Initial flip 2 cards test_game.py:454-469
Initial flip 0 skips phase test_game.py:471-478
Game starts after all flip test_game.py:480-491

Card Values

Card Points Notes
Ace 1 Low card
2 -2 Negative! Best non-special card
3-10 Face value 3=3, 4=4, ..., 10=10
Jack 10 Face card
Queen 10 Face card
King 0 Zero points
Joker -2 Negative (requires use_jokers option)

Card Value Quality Tiers

Tier Cards Strategy
Excellent Joker (-2), 2 (-2) Always keep, never pair
Good King (0) Safe, good for pairing
Decent Ace (1) Low risk
Neutral 3, 4, 5 Acceptable
Bad 6, 7 Replace when possible
Terrible 8, 9, 10, J, Q High priority to replace
Implementation File
DEFAULT_CARD_VALUES constants.py:3-18
RANK_VALUES derivation game.py:40-41
get_card_value() game.py:44-67
Tests File
Ace worth 1 test_game.py:29-30
Two worth -2 test_game.py:32-33
3-10 face value test_game.py:35-43
Jack worth 10 test_game.py:45-46
Queen worth 10 test_game.py:48-49
King worth 0 test_game.py:51-52
Joker worth -2 test_game.py:54-55
Card.value() method test_game.py:57-63
Rank quality classification test_analyzer.py:47-74

Column Pairing

Critical Rule: If both cards in a column have the same rank, that column scores 0 points regardless of the individual card values.

Column Positions

Column 0: positions (0, 3)
Column 1: positions (1, 4)
Column 2: positions (2, 5)

Examples

Matched column:

[K] [5] [7]    K-K pair = 0
[K] [3] [9]    5+3 = 8, 7+9 = 16
               Total: 0 + 8 + 16 = 24

All columns matched:

[A] [5] [K]    All paired = 0 total
[A] [5] [K]

Edge Case: Paired Negative Cards

IMPORTANT: Paired 2s score 0, not -4. The pair cancels the value, it doesn't double it.

This is a common source of bugs. When two 2s are paired:

  • Individual values: -2 + -2 = -4
  • Paired value: 0 (pair rule overrides)

The same applies to paired Jokers (standard rules) - they score 0, not -4.

Implementation File
Column pair detection game.py:158-178
Pair cancels to 0 game.py:174-175
Tests File
Matching column scores 0 test_game.py:83-91
All columns matched = 0 test_game.py:93-98
No columns matched = sum test_game.py:100-106
Paired 2s = 0 (not -4) test_game.py:108-115
Unpaired negatives keep value test_game.py:117-124

Turn Structure

1. Draw Phase

Choose ONE:

  • Draw the top card from the draw pile (face-down deck)
  • Take the top card from the discard pile (face-up)

2. Play Phase

If you drew from the DECK:

  • Swap: Replace any card in your grid (old card goes to discard face-up)
  • Discard: Put the drawn card on the discard pile (optionally flip a face-down card)

If you took from the DISCARD PILE:

  • You MUST swap - you cannot re-discard the same card
  • Replace any card in your grid (old card goes to discard)

Important Rules

  • Swapped cards are always placed face-up
  • You cannot look at a face-down card before deciding to replace it
  • When swapping a face-down card, reveal it only as it goes to discard
Implementation File
Draw from deck/discard game.py:354-384
Swap card game.py:409-426
Cannot re-discard from discard game.py:428-433, game.py:443-445
Discard from deck draw game.py:435-460
Flip after discard game.py:462-476
Tests File
Can draw from deck test_game.py:200-205
Can draw from discard test_game.py:207-214
Can discard deck draw test_game.py:216-221
Cannot discard discard draw test_game.py:223-228
Must swap discard draw test_game.py:230-238
Swap makes card face-up test_game.py:240-247
Cannot peek before swap test_game.py:249-256

Round End

Triggering the Final Turn

When any player has all 6 cards face-up, the round enters "final turn" phase.

Final Turn Phase

  • Each other player gets exactly one more turn
  • The player who triggered final turn does NOT get another turn
  • After all players have had their final turn, the round ends

Scoring

  1. All remaining face-down cards are revealed
  2. Calculate each player's score (with column pairing)
  3. Add round score to total score
Implementation File
Check all face-up game.py:478-483
Final turn phase game.py:488-502
End round scoring game.py:504-555
Tests File
Revealing all triggers final turn test_game.py:327-341
Other players get final turn test_game.py:343-358
Finisher doesn't get extra turn test_game.py:360-373
All cards revealed at round end test_game.py:375-388

Winning

  • Standard game: 9 rounds ("9 holes")
  • Player with the lowest total score wins
  • Optionally play 18 rounds for a longer game
Implementation File
Multi-round tracking game.py:557-567
Total score accumulation game.py:548-549
Tests File
Next round resets hands test_game.py:398-418
Scores accumulate across rounds test_game.py:420-444

Part 2: House Rules

Our implementation supports these optional rule variations. All are disabled by default.

Standard Options

Option Description Default
initial_flips Cards revealed at start (0, 1, or 2) 2
flip_mode What happens when discarding from deck (see below) never
knock_penalty +10 if you go out but don't have lowest score Off
use_jokers Add Jokers to deck (-2 points each) Off

Flip Mode Options

The flip_mode setting controls what happens when you draw from the deck and choose to discard (not swap):

Value Name Behavior
never Standard No flip when discarding - your turn ends immediately. This is the classic rule.
always Speed Golf Must flip one face-down card when discarding. Accelerates the game by revealing more information each turn.
endgame Endgame Flip after discard if any player has 1 hidden card remaining.

Standard (never): When you draw from the deck and choose not to use the card, simply discard it and your turn ends.

Speed Golf (always): When you discard from the deck, you must also flip one of your face-down cards. This accelerates the game by revealing more information each turn, leading to faster rounds.

Endgame: When any player has only 1 (or 0) face-down cards remaining, discarding from the deck triggers a flip. This accelerates the endgame by revealing more information as rounds approach their conclusion.

Implementation File
GameOptions dataclass game.py:200-222
FlipMode enum game.py:12-24
flip_on_discard property game.py:449-470
flip_is_optional property game.py:472-479
skip_flip_and_end_turn() game.py:520-540

Point Modifiers

Option Effect Standard Value Modified Value
lucky_swing Single Joker in deck 2 Jokers @ -2 each 1 Joker @ -5
super_kings Kings are negative King = 0 King = -2
ten_penny 10s are low 10 = 10 10 = 1
Implementation File
LUCKY_SWING_JOKER_VALUE constants.py:23
SUPER_KINGS_VALUE constants.py:21
TEN_PENNY_VALUE constants.py:22
Value application game.py:58-66
Tests File
Super Kings -2 test_game.py:142-149
Ten Penny test_game.py:151-158
Lucky Swing Joker -5 test_game.py:160-173
Lucky Swing single joker test_game.py:511-514

Bonuses & Penalties

Option Effect When Applied
knock_bonus First to reveal all cards gets -5 Round end
underdog_bonus Lowest scorer each round gets -3 Round end
tied_shame Tying another player's score = +5 penalty to both Round end
blackjack Exact score of 21 becomes 0 Round end
wolfpack 2 pairs of Jacks = -5 bonus Scoring
Implementation File
Blackjack 21->0 game.py:513-517
Knock penalty game.py:519-525
Knock bonus game.py:527-531
Underdog bonus game.py:533-538
Tied shame game.py:540-546
Wolfpack game.py:180-182
Tests File
Blackjack 21 becomes 0 test_game.py:175-183
House rules integration test_house_rules.py (full file)

Special Rules

Option Effect
eagle_eye Jokers worth +2 unpaired, -4 paired (reward spotting pairs)
Implementation File
Eagle eye unpaired value game.py:60-61
Eagle eye paired value game.py:169-173

Part 3: AI Decision Making

AI Profiles

8 distinct AI personalities with different play styles:

Name Style Swap Threshold Pair Hope Aggression
Sofia Calculated & Patient 4 0.2 0.2
Maya Aggressive Closer 6 0.4 0.85
Priya Pair Hunter 7 0.8 0.5
Marcus Steady Eddie 5 0.35 0.4
Kenji Risk Taker 8 0.7 0.75
Diego Chaotic Gambler 6 0.5 0.6
River Adaptive Strategist 5 0.45 0.55
Sage Sneaky Finisher 5 0.3 0.9
Implementation File
CPUProfile dataclass ai.py:164-182
CPU_PROFILES list ai.py:186-253

Key AI Decision Functions

should_take_discard()

Decides whether to take from discard pile or draw from deck.

Logic priority:

  1. Always take Jokers (and pair if Eagle Eye)
  2. Always take Kings
  3. Take 10s if ten_penny enabled
  4. Take cards that complete a column pair (except negative cards)
  5. Take low cards based on game phase threshold
  6. Consider end-game pressure
  7. Take if we have worse visible cards
Implementation File
should_take_discard() ai.py:333-412
Negative card pair avoidance ai.py:365-374
Tests File
Maya doesn't take 10 with good hand test_maya_bug.py:52-83
Unpredictability doesn't take bad cards test_maya_bug.py:85-116
Pair potential respected test_maya_bug.py:289-315

choose_swap_or_discard()

Decides whether to swap the drawn card into hand or discard it.

Logic priority:

  1. Eagle Eye: Pair Jokers if visible match exists
  2. Check for column pair opportunity (except negative cards)
  3. Find best swap among BAD face-up cards (positive value)
  4. Consider Blackjack (21) pursuit
  5. Swap excellent cards into face-down positions
  6. Apply profile-based thresholds

Critical: When placing cards into face-down positions, the AI must avoid creating wasteful pairs with visible negative cards.

Implementation File
choose_swap_or_discard() ai.py:414-536
Negative card pair avoidance ai.py:441-446
Tests File
Don't discard excellent cards test_maya_bug.py:179-209
Full Maya bug scenario test_maya_bug.py:211-254

Part 4: Edge Cases & Known Issues

Edge Case: Pairing Negative Cards

Problem: Pairing 2s or Jokers wastes their negative value.

  • Unpaired 2: contributes -2 to score
  • Paired 2s: contribute 0 to score (lost 2 points!)

AI Safeguards:

  1. should_take_discard(): Only considers pairing if discard_value > 0
  2. choose_swap_or_discard(): Sets should_pair = drawn_value > 0
  3. filter_bad_pair_positions(): Filters out positions that would create wasteful pairs when placing negative cards into face-down positions
Implementation File
get_column_partner_position() ai.py:163-168
filter_bad_pair_positions() ai.py:171-213
Applied in choose_swap_or_discard ai.py:517, ai.py:538
Applied in forced swap ai.py:711-713
Tests File
Paired 2s = 0 (game logic) test_game.py:108-115
AI avoids pairing logic ai.py:365-374, ai.py:441-446
Filter with visible two test_maya_bug.py:320-347
Filter allows positive pairs test_maya_bug.py:349-371
Choose swap avoids 2 pairs test_maya_bug.py:373-401
Forced swap avoids 2 pairs test_maya_bug.py:403-425
Fallback when all bad test_maya_bug.py:427-451

Edge Case: Forced Swap from Discard

When drawing from discard pile and choose_swap_or_discard() returns None (discard), the AI is forced to swap anyway. The fallback picks randomly from face-down positions, or finds the worst face-up card.

Implementation File
Forced swap fallback ai.py:665-686
Tests File
Forced swap uses house rules test_maya_bug.py:143-177
All face-up finds worst test_maya_bug.py:260-287

Edge Case: Deck Exhaustion

When the deck is empty, the discard pile (except top card) is reshuffled back into the deck.

Implementation File
Reshuffle discard pile game.py:386-407

Edge Case: Empty Discard Pile

Cannot draw from empty discard pile.

Tests File
Empty discard returns None test_game.py:558-569

Part 5: Test Coverage Summary

Test Files

File Tests Focus
test_game.py 44 Core game rules
test_house_rules.py 10+ House rule integration
test_analyzer.py 18 AI decision evaluation
test_maya_bug.py 18 Bug regression & AI edge cases

Total: 83+ tests

Coverage by Category

Category Tests Files Status
Card Values 8 test_game.py, test_analyzer.py Complete
Column Pairing 5 test_game.py Complete
House Rules Scoring 4 test_game.py Complete
Draw/Discard Mechanics 7 test_game.py Complete
Turn Flow 4 test_game.py Complete
Round End 4 test_game.py Complete
Multi-Round 2 test_game.py Complete
Initial Flip 3 test_game.py Complete
Deck Management 4 test_game.py Complete
Edge Cases 3 test_game.py Complete
Take Discard Evaluation 6 test_analyzer.py Complete
Swap Evaluation 6 test_analyzer.py Complete
House Rules Evaluation 2 test_analyzer.py Complete
Maya Bug Regression 6 test_maya_bug.py Complete
AI Edge Cases 3 test_maya_bug.py Complete
Bad Pair Avoidance 5 test_maya_bug.py Complete

Test Plan: Critical Paths

Game State Transitions

WAITING -> INITIAL_FLIP -> PLAYING -> FINAL_TURN -> ROUND_OVER -> GAME_OVER
                |                         ^
                v (initial_flips=0)       |
                +-------------------------+
Transition Trigger Tests
WAITING -> INITIAL_FLIP start_game() test_game.py:454-469
WAITING -> PLAYING start_game(initial_flips=0) test_game.py:471-478
INITIAL_FLIP -> PLAYING All players flip test_game.py:480-491
PLAYING -> FINAL_TURN Player all face-up test_game.py:327-341
FINAL_TURN -> ROUND_OVER All final turns done test_game.py:343-358
ROUND_OVER -> PLAYING start_next_round() test_game.py:398-418
ROUND_OVER -> GAME_OVER Final round complete test_game.py:420-444

AI Decision Tree

Draw Phase:
  ├── should_take_discard() returns True
  │   └── Draw from discard pile
  │       └── MUST swap (can_discard_drawn=False)
  └── should_take_discard() returns False
      └── Draw from deck
          ├── choose_swap_or_discard() returns position
          │   └── Swap at position
          └── choose_swap_or_discard() returns None
              └── Discard drawn card
                  └── flip_on_discard?
                      ├── flip_mode="always" -> MUST flip (choose_flip_after_discard)
                      └── flip_mode="endgame" -> should_skip_optional_flip()?
                          ├── True -> skip flip, end turn
                          └── False -> flip (choose_flip_after_discard)
Decision Point Tests
Take Joker/King from discard test_analyzer.py:96-114
Don't take bad cards test_maya_bug.py:52-116
Swap excellent cards test_maya_bug.py:179-209
Avoid pairing negatives test_maya_bug.py:320-451
Forced swap from discard test_maya_bug.py:143-177, test_maya_bug.py:403-425

Scoring Edge Cases

Scenario Expected Test
Paired 2s 0 (not -4) test_game.py:108-115
Paired Jokers (standard) 0 Implicit
Paired Jokers (eagle_eye) -4 game.py:169-173
Unpaired negative cards -2 each test_game.py:117-124
All columns matched 0 total test_game.py:93-98
Blackjack (21) 0 test_game.py:175-183

Running Tests

# Run all tests
cd server && python -m pytest -v

# Run specific test file
python -m pytest test_game.py -v

# Run specific test class
python -m pytest test_game.py::TestCardValues -v

# Run with coverage
python -m pytest --cov=. --cov-report=html

# Run tests matching pattern
python -m pytest -k "pair" -v

Test Quality Checklist

  • All card values verified against RULES.md
  • Column pairing logic tested (including negatives)
  • House rules tested individually
  • Draw/discard constraints enforced
  • Turn flow and player validation
  • Round end and final turn logic
  • Multi-round score accumulation
  • AI decision quality evaluation
  • Bug regression tests for Maya bug
  • AI avoids wasteful negative card pairs

Disadvantageous Moves (AI Quality Metrics)

Definition: "Dumb Moves"

Moves that are objectively suboptimal and should occur at minimal background noise level (< 1% of opportunities).

Move Type Severity Expected Prevalence Test Coverage
Discarding Joker/2 Blunder 0% test_maya_bug.py:179-209
Discarding King Mistake 0% test_analyzer.py:183-192
Taking 10/J/Q without pair Blunder 0% test_maya_bug.py:52-116
Pairing negative cards Mistake 0% test_maya_bug.py:373-401
Swapping good card for bad Mistake 0% test_analyzer.py:219-237

Definition: "Questionable Moves"

Moves that may be suboptimal but have legitimate strategic reasons. Should be < 5% of opportunities.

Move Type When Acceptable Monitoring
Not taking low card (3-5) Pair hunting, early game Profile-based
Discarding medium card (4-6) Full hand, pair potential Context check
Going out with high score Pressure, knock_bonus Threshold based

AI Quality Assertions

These assertions should pass when running extended simulations:

# In test suite or simulation
def test_ai_quality_metrics():
    """Run N games and verify dumb moves are at noise level."""
    stats = run_simulation(games=1000)

    # ZERO tolerance blunders
    assert stats.discarded_jokers == 0
    assert stats.discarded_twos == 0
    assert stats.took_bad_card_without_pair == 0
    assert stats.paired_negative_cards == 0

    # Near-zero tolerance mistakes
    assert stats.discarded_kings < stats.total_turns * 0.001  # < 0.1%
    assert stats.swapped_good_for_bad < stats.total_turns * 0.001

    # Acceptable variance
    assert stats.questionable_moves < stats.total_turns * 0.05  # < 5%

Tracking Implementation

Decision quality should be logged for analysis:

Field Description
decision_type take_discard, swap, discard, flip
decision_quality OPTIMAL, GOOD, QUESTIONABLE, MISTAKE, BLUNDER
expected_value EV calculation for the decision
profile_name AI personality that made decision
game_phase early, mid, late

See game_analyzer.py for decision evaluation logic.

Area Description Priority
AI Quality Metrics Simulation-based dumb move detection Critical
WebSocket Integration tests for real-time communication High
Concurrent games Multiple simultaneous rooms Medium
Deck exhaustion Reshuffle when deck empty Medium
All house rule combos Interaction between rules Medium
AI personality variance Verify distinct behaviors Low
Performance Load testing with many players Low

Part 6: Strategic Notes

Card Priority (for AI and Players)

Always Keep

  • Jokers (-2 or -5): Best cards in game
  • 2s (-2): Second best, but don't pair them!

Keep When Possible

  • Kings (0): Safe, excellent for pairing
  • Aces (1): Low risk

Replace When Possible

  • 6, 7 (6-7 points): Moderate priority
  • 8, 9 (8-9 points): High priority
  • 10, J, Q (10 points): Highest priority

Pairing Strategy

  • Pairing is powerful - column score goes to 0
  • Never pair negative cards (2s, Jokers) - you lose the negative benefit
  • Target pairs with mid-value cards (3-7) for maximum gain
  • High-value pairs (10, J, Q) are valuable (+20 point swing)

When to Go Out

  • Go out with score <= 10 when confident you're lowest
  • Consider opponent visible cards before going out early
  • With knock_penalty, be careful - +10 hurts if you're wrong
  • With knock_bonus, more incentive to finish first

Part 7: Configuration

Configuration Files

File Purpose
pyproject.toml Project metadata, dependencies, tool config
server/config.py Centralized configuration loader
server/constants.py Card values and game constants
.env.example Environment variable documentation
.env Local environment overrides (not committed)

Environment Variables

Configuration precedence (highest to lowest):

  1. Environment variables
  2. .env file
  3. Default values in code

Server Settings

Variable Default Description
HOST 0.0.0.0 Host to bind to
PORT 8000 Port to listen on
DEBUG false Enable debug mode
LOG_LEVEL INFO Logging level
DATABASE_URL sqlite:///games.db Database connection

Room Settings

Variable Default Description
MAX_PLAYERS_PER_ROOM 6 Max players per game
ROOM_TIMEOUT_MINUTES 60 Inactive room cleanup
ROOM_CODE_LENGTH 4 Room code length

Game Defaults

Variable Default Description
DEFAULT_ROUNDS 9 Rounds per game
DEFAULT_INITIAL_FLIPS 2 Cards to flip at start
DEFAULT_USE_JOKERS false Enable jokers
DEFAULT_FLIP_MODE never Flip mode: never, always, or endgame

Security

Variable Default Description
SECRET_KEY (empty) Secret key for sessions
INVITE_ONLY false Require invitation to register
ADMIN_EMAILS (empty) Comma-separated admin emails

Running the Server

# Development (with auto-reload)
DEBUG=true python server/main.py

# Production
PORT=8080 LOG_LEVEL=WARNING python server/main.py

# With .env file
cp .env.example .env
# Edit .env as needed
python server/main.py

# Using uvicorn directly
uvicorn server.main:app --host 0.0.0.0 --port 8000

Part 8: Authentication

Overview

The authentication system supports:

  • User accounts stored in SQLite (users table)
  • Admin accounts that can manage other users
  • Invite codes (or room codes) for registration
  • Session-based authentication with bearer tokens

First-Time Setup

When the server starts with no admin accounts:

  1. A default admin account is created (or accounts for each email in ADMIN_EMAILS)
  2. The admin account has no password initially
  3. On first login attempt, use /api/auth/setup-password to set the password
# Check if admin needs setup
curl http://localhost:8000/api/auth/check-setup/admin

# Set admin password (first time only)
curl -X POST http://localhost:8000/api/auth/setup-password \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "new_password": "your-secure-password"}'

API Endpoints

Public Endpoints

Endpoint Method Description
/api/auth/check-setup/{username} GET Check if user needs password setup
/api/auth/setup-password POST Set password (first login only)
/api/auth/login POST Login with username/password
/api/auth/register POST Register with invite code
/api/auth/logout POST Logout current session

Authenticated Endpoints

Endpoint Method Description
/api/auth/me GET Get current user info
/api/auth/password PUT Change own password

Admin Endpoints

Endpoint Method Description
/api/admin/users GET List all users
/api/admin/users/{id} GET Get user by ID
/api/admin/users/{id} PUT Update user
/api/admin/users/{id}/password PUT Change user password
/api/admin/users/{id} DELETE Deactivate user
/api/admin/invites POST Create invite code
/api/admin/invites GET List invite codes
/api/admin/invites/{code} DELETE Deactivate invite code

Registration Flow

  1. User obtains an invite code (from admin) or a room code (from active game)
  2. User calls /api/auth/register with username, password, and invite code
  3. If valid, account is created and session token is returned
# Register with room code
curl -X POST http://localhost:8000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"username": "player1", "password": "pass123", "invite_code": "ABCD"}'

Authentication Header

After login, include the token in requests:

Authorization: Bearer <token>

Database Schema

-- Users table
CREATE TABLE users (
    id TEXT PRIMARY KEY,
    username TEXT UNIQUE NOT NULL,
    email TEXT UNIQUE,
    password_hash TEXT NOT NULL,  -- SHA-256 with salt
    role TEXT DEFAULT 'user',     -- 'user' or 'admin'
    created_at TIMESTAMP,
    last_login TIMESTAMP,
    is_active BOOLEAN DEFAULT 1,
    invited_by TEXT
);

-- Sessions table
CREATE TABLE sessions (
    token TEXT PRIMARY KEY,
    user_id TEXT REFERENCES users(id),
    created_at TIMESTAMP,
    expires_at TIMESTAMP NOT NULL
);

-- Invite codes table
CREATE TABLE invite_codes (
    code TEXT PRIMARY KEY,
    created_by TEXT REFERENCES users(id),
    created_at TIMESTAMP,
    expires_at TIMESTAMP,
    max_uses INTEGER DEFAULT 1,
    use_count INTEGER DEFAULT 0,
    is_active BOOLEAN DEFAULT 1
);
Implementation File
AuthManager class auth.py:87-460
User model auth.py:27-50
Password hashing auth.py:159-172
Session management auth.py:316-360
Tests File
User creation test_auth.py:22-60
Authentication test_auth.py:63-120
Invite codes test_auth.py:123-175
Admin functions test_auth.py:178-220

Last updated: Document generated from codebase analysis Reference implementations: config.py, constants.py, game.py, ai.py, auth.py Test suites: test_game.py, test_house_rules.py, test_analyzer.py, test_maya_bug.py, test_auth.py