From a074ff77b19424f7bcdae77ac3c2d0985f02b5b8 Mon Sep 17 00:00:00 2001 From: Tim McCarthy Date: Thu, 11 Jun 2026 21:32:00 -0700 Subject: [PATCH] Fable will fix it! Pt 2 --- .gitignore | 3 + README.md | 40 +- frontend/src/assets/hero.png | Bin 13057 -> 0 bytes frontend/src/assets/svelte.svg | 1 - frontend/src/assets/vite.svg | 1 - frontend/src/components/Card.svelte | 52 + .../components/CharacterCreationPhase.svelte | 10 +- frontend/src/components/EventLog.svelte | 280 ++++ frontend/src/components/ScenePhase.svelte | 714 +-------- frontend/src/components/UpkeepPhase.svelte | 53 +- .../components/scene/ChallengePanel.svelte | 143 ++ .../components/scene/CharacterSheet.svelte | 156 ++ .../components/scene/DeepControlPanel.svelte | 152 ++ .../src/components/scene/ObstacleBoard.svelte | 131 ++ frontend/src/lib/Counter.svelte | 5 - frontend/src/lib/cards.js | 27 + frontend/src/lib/suggestions.js | 241 +-- src/pirats/cards.py | 249 ++- src/pirats/crud_base.py | 102 +- src/pirats/crud_challenge.py | 82 +- src/pirats/crud_character.py | 12 +- src/pirats/crud_scene.py | 98 +- src/pirats/crud_upkeep.py | 13 +- src/pirats/database.py | 1 + src/pirats/main.py | 50 +- src/pirats/models.py | 1 + src/pirats/routes_admin.py | 1 - src/pirats/routes_character.py | 9 +- src/pirats/routes_lobby.py | 1 - src/pirats/routes_scene.py | 1 - src/pirats/routes_upkeep.py | 2 +- src/pirats/static/css/admin.css | 28 - src/pirats/static/css/card.css | 244 --- src/pirats/static/css/character.css | 442 ----- src/pirats/static/css/components.css | 301 ---- src/pirats/static/css/core.css | 231 --- src/pirats/static/css/lobby.css | 106 -- src/pirats/static/css/scene-play.css | 277 ---- src/pirats/static/css/scene-setup.css | 83 - src/pirats/static/css/style.css | 18 - src/pirats/static/css/upkeep.css | 100 -- src/pirats/static/css/welcome.css | 50 - src/pirats/static/js/suggestions.js | 1423 ----------------- tests/test_game.py | 118 +- 44 files changed, 1547 insertions(+), 4505 deletions(-) delete mode 100644 frontend/src/assets/hero.png delete mode 100644 frontend/src/assets/svelte.svg delete mode 100644 frontend/src/assets/vite.svg create mode 100644 frontend/src/components/Card.svelte create mode 100644 frontend/src/components/EventLog.svelte create mode 100644 frontend/src/components/scene/ChallengePanel.svelte create mode 100644 frontend/src/components/scene/CharacterSheet.svelte create mode 100644 frontend/src/components/scene/DeepControlPanel.svelte create mode 100644 frontend/src/components/scene/ObstacleBoard.svelte delete mode 100644 frontend/src/lib/Counter.svelte create mode 100644 frontend/src/lib/cards.js delete mode 100644 src/pirats/static/css/admin.css delete mode 100644 src/pirats/static/css/card.css delete mode 100644 src/pirats/static/css/character.css delete mode 100644 src/pirats/static/css/components.css delete mode 100644 src/pirats/static/css/core.css delete mode 100644 src/pirats/static/css/lobby.css delete mode 100644 src/pirats/static/css/scene-play.css delete mode 100644 src/pirats/static/css/scene-setup.css delete mode 100644 src/pirats/static/css/style.css delete mode 100644 src/pirats/static/css/upkeep.css delete mode 100644 src/pirats/static/css/welcome.css delete mode 100644 src/pirats/static/js/suggestions.js diff --git a/.gitignore b/.gitignore index cc71236..510a85d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ .venv rats_with_gats.db +smoke_test.db result *.pdf +# Built Svelte frontend (copied in from frontend/dist by the Nix build) +src/pirats/static/ diff --git a/README.md b/README.md index dc1fffe..c031045 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Rats with Gats Remote Play (`pirats`) -A web-based remote play companion app for the tabletop roleplaying game **Rats with Gats** (where players roleplay as space-faring pirate rats, or "Pi-Rats"). The application manages lobbies, character creation, card mechanics, scene phases, obstacles, challenges, and player voting. +A web-based remote play companion app for the tabletop roleplaying game **Rats with Gats** (where players roleplay as gunslinging pirate rats, or "Pi-Rats", in the magical land of Yeld). The application manages lobbies, character creation, card mechanics, scene phases, obstacles, challenges, and player voting. See [RULEBOOK.md](RULEBOOK.md) for the game rules. -It is built with **FastAPI**, **SQLModel** (SQLite database), **Jinja2 templates**, and **HTMX** for dynamic, real-time page updates. +It is built with a **FastAPI** + **SQLModel** (SQLite) backend and a **Svelte 5** single-page frontend (in `frontend/`), which polls the backend's JSON state endpoint for near-real-time updates. --- @@ -125,16 +125,30 @@ nix develop --command pytest ``` pirats/ -├── pyproject.toml # Python package metadata and dependencies -├── flake.nix # Nix package, dev shell, and NixOS service module -├── tests/ # Unit and integration tests -│ └── test_game.py # Game logic and CRUD database test suites +├── pyproject.toml # Python package metadata and dependencies +├── flake.nix # Nix package (incl. frontend build), dev shell, NixOS service module +├── tests/ +│ └── test_game.py # Game logic and CRUD test suite (pure-crud, no HTTP for most) +├── frontend/ # Svelte 5 + Vite SPA +│ └── src/ +│ ├── pages/ # Routes: Home, Join, Dashboard (game UI), Admin +│ ├── components/ # One component per game phase, Card.svelte, and scene/ (ScenePhase sub-panels) +│ └── lib/ # api.js (fetch wrapper), cards.js (card display helpers), suggestions.js (Suggest-button pools) └── src/ - └── pirats/ # Core Python package - ├── main.py # FastAPI routes, HTTP endpoints, CLI entrypoint - ├── crud.py # Core database CRUD, game mechanics, and transitions - ├── models.py # SQLModel database tables (Game, Player, Obstacle, Challenge, Vote) - ├── cards.py # Playing card parser, deck setups, and obstacle definitions - ├── templates/ # Jinja2 HTML pages and HTMX snippets - └── static/ # CSS styles and frontend assets + └── pirats/ # FastAPI backend package + ├── main.py # App setup, /api/game create/join/state endpoints, CLI entrypoint + ├── models.py # SQLModel tables (Game, Player, Obstacle, Challenge, Vote, GameEvent) + ├── cards.py # Card parsing, deck building, obstacle table from the rulebook + ├── crud.py # Facade re-exporting all crud_* modules + ├── crud_*.py # Game logic by phase: base (deck/hands/rank), character, scene, challenge, upkeep + ├── routes_*.py # Thin API routers by phase, mounted under /api + └── static/ # Built frontend lands here (gitignored; populated by the Nix build) +``` + +### Frontend Development + +Run the backend (`pirats --reload`) and the Vite dev server side by side; Vite proxies `/api` to port 8000: + +```bash +cd frontend && npm install && npm run dev ``` diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png deleted file mode 100644 index 02251f4b956c55af2d76fd0788124d7eee2b45eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13057 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 diff --git a/frontend/src/assets/svelte.svg b/frontend/src/assets/svelte.svg deleted file mode 100644 index c5e0848..0000000 --- a/frontend/src/assets/svelte.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/frontend/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/frontend/src/components/Card.svelte b/frontend/src/components/Card.svelte new file mode 100644 index 0000000..5763022 --- /dev/null +++ b/frontend/src/components/Card.svelte @@ -0,0 +1,52 @@ + + +{#if size === 'mini'} +
+ {val} + {suit} + {#if owner}{owner}{/if} +
+{:else} +
+
+ {val} + {suit} +
+
+ {#if joker} + 🃏 + {:else} + {suit} + {/if} +
+ {#if techName} +
+
{cardValue(card)}: "{techName}"
+
+ {/if} +
+ {val} + {suit} +
+
+{/if} diff --git a/frontend/src/components/CharacterCreationPhase.svelte b/frontend/src/components/CharacterCreationPhase.svelte index 172d1ac..9f5834f 100644 --- a/frontend/src/components/CharacterCreationPhase.svelte +++ b/frontend/src/components/CharacterCreationPhase.svelte @@ -1,7 +1,7 @@ + +
+ + + {#if open} +
+ {#if events.length} +
+ {#if loadingOlder} + Hauling up older entries… + {:else if haveMore} + + {:else} + ⚓ The story begins here + {/if} +
+ {#each events as event (event.id)} +
+ {KIND_ICONS[event.kind] || KIND_ICONS.info} + {event.message} + {formatTime(event.timestamp)} +
+ {/each} + {:else} +
No events yet.
+ {/if} +
+ + {#if !atBottom} + + {/if} + {/if} +
+ + diff --git a/frontend/src/components/ScenePhase.svelte b/frontend/src/components/ScenePhase.svelte index 40f59d2..0aae20a 100644 --- a/frontend/src/components/ScenePhase.svelte +++ b/frontend/src/components/ScenePhase.svelte @@ -1,231 +1,17 @@
@@ -274,280 +60,16 @@
- {#if error} -
{error}
- {/if} - - - {#each openChallenges as ch} - {@const chPlays = JSON.parse(ch.plays || '[]')} - {@const chObstacleIds = JSON.parse(ch.obstacle_ids || '[]')} -
-
-

- {#if ch.challenge_type === 'pvp'} - ⚔️ Duel: {playerName(ch.challenger_player_id)} vs {playerName(ch.target_player_id)} - {:else} - ⚔️ The Deep challenges {playerName(ch.target_player_id)}! - {/if} -

- {#if state.player.role === 'deep' && ch.challenge_type === 'deep'} -
- - -
- {/if} -
- {#if ch.stakes} -

Stakes: {ch.stakes}

- {/if} - {#if ch.challenge_type === 'pvp'} -

- Temporary Obstacle: {getCardDisplay(ch.temp_card)} — {playerName(ch.acting_player_id)} must answer it! -

- {:else} -

- Applied Obstacles: {chObstacleIds.map(oid => state.obstacles.find(o => o.id === oid)?.title || '(discarded)').join(', ') || 'None left'} - — attempted by {playerName(ch.acting_player_id)}{ch.acting_player_id !== ch.target_player_id ? ` (took it over via ${ch.tax_type === 'gat' ? 'Gat' : 'Name'} Tax)` : ''}. - Other Pi-Rats may assist (assistants don't draw back). -

- {/if} - {#if chPlays.length > 0} -

- Plays: {chPlays.map(p => `${p.player_name}: ${getCardDisplay(p.card)} ${p.success ? '✔' : '✘'}`).join(' · ')} -

- {/if} - - - {#if ch.tax_state === 'requested'} - {#if myTaxRequest && myTaxRequest.id === ch.id} -
-

{playerName(ch.target_player_id)} calls a {ch.tax_type === 'gat' ? 'Gat' : 'Name'} Tax on you: take this Challenge for them!

-

Accept: you get one random card from their hand and attempt the Challenge. Refuse: you hand over your {ch.tax_type === 'gat' ? 'Gat' : 'Name'} — they keep it if they succeed!

-
- - -
-
- {:else} -

⏳ Waiting for {playerName(ch.tax_target_id)} to answer the {ch.tax_type === 'gat' ? 'Gat' : 'Name'} Tax...

- {/if} - {/if} - - - {#if ch.challenge_type === 'deep' && ch.acting_player_id === state.player.id && !ch.tax_state && !state.player.tax_banned && taxType(state.player) && eligibleTaxTargets(ch).length > 0} -
- No {taxType(state.player)}? Make it someone else's problem: - - -
- {/if} - - - {#if myPvpDefense && myPvpDefense.id === ch.id} -
-

Defend yourself! Pick a card:

-
- {#each hand.filter(c => !isJoker(c)) as card} - - {/each} -
-
- {/if} -
- {/each} - - {#each state.obstacles as obs} - {@const column_cards = JSON.parse(obs.played_cards || '[]')} - {@const active_card_code = column_cards.length > 0 ? column_cards[column_cards.length - 1].card : obs.original_card} - {@const success_count = column_cards.filter(c => c.success).length} - {@const is_completed = success_count >= state.players.length} - {@const in_challenge = challengedObstacleIds.has(obs.id)} -
{ if (!is_completed) e.preventDefault(); }} - on:dragenter={(e) => { if (!is_completed) e.currentTarget.classList.add("drag-over"); }} - on:dragleave={(e) => { if (!is_completed) e.currentTarget.classList.remove("drag-over"); }} - on:drop={(e) => { - if (is_completed) return; - e.preventDefault(); - e.currentTarget.classList.remove("drag-over"); - const cardCode = e.dataTransfer.getData('text/plain'); - if (cardCode) { - if (isJoker(cardCode)) playJoker(obs.id, cardCode); - else playCard(obs.id, cardCode); - } - }}> -
-
-
- {active_card_code.slice(0, -1)} - {{'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[active_card_code.slice(-1)]} -
- -
- {{'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[active_card_code.slice(-1)]} -
- -
- {active_card_code.slice(0, -1)} - {{'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[active_card_code.slice(-1)]} -
-
-
-
-

{obs.title} {#if in_challenge}⚔️ In Challenge{/if}

-

{obs.description}

-
- - -
- Current Difficulty - - {#if ['J', 'Q', 'K'].includes((column_cards.length > 0 ? column_cards[column_cards.length - 1].card : obs.original_card).slice(0, -1))} - Challenged Rat's Rank - {:else} - {obs.current_value} - {/if} - -
- - -
- Successes - - {success_count} / {state.players.length} - -
- - -
-
Card History (Column)
-
-
- {obs.original_card.slice(0, -1)} - {{'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[obs.original_card.slice(-1)]} -
- {#each column_cards as pc} -
- {pc.card.slice(0, -1)} - {{'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[pc.card.slice(-1)]} - {pc.player_name.slice(0,4)} -
- {/each} -
-
- - {#if is_completed && state.player.role === 'deep'} -
- -
- {/if} -
- {:else} -

No active obstacles. Deep players can end the scene!

- {/each} - + +
- {#if state.player.role === "deep"} -
-

🌊 Deep Control Panel

- - -
-

⚔️ Call a Challenge

-

Establish the stakes, pick a Pi-Rat, and apply one or more Obstacles. They play one card per Obstacle — beating one passes; each failure breeds a complication.

- -
- {#each state.obstacles as obs} - {@const taken = challengedObstacleIds.has(obs.id)} - - {/each} -
- - -
- - -
-

🏴‍☠️ Captaincy

-

The Captain holds power until they step down, lose a duel, die, or the ship is lost. Reassign after a leadership duel or resignation.

-
- - -
-
- -
-

Crew Objectives

-
- - - -
-
- -
-

Pi-Rat Personal Objectives

-

You control completion. Only mark in sequence (1 -> 2 -> 3).

- {#each state.players.filter(p => p.role === 'pirat') as p} -
- {p.name} -
- - - -
-
- {/each} -
- - -
-

End the scene once every Pi-Rat has faced a Challenge and had a chance at an Objective, or the Obstacle List is empty.

- -
-
+ {/if} @@ -557,219 +79,25 @@
{#each hand as card} -
{ - e.dataTransfer.setData('text/plain', card); - e.currentTarget.classList.add("dragging"); - }} - on:dragend={(e) => { - e.currentTarget.classList.remove("dragging"); - }}> -
- {isJoker(card) ? '🃏' : card.slice(0, -1)} - {isJoker(card) ? '' : {'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[card.slice(-1)]} -
- -
- {#if isJoker(card)} - 🃏 - {:else} - {{'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[card.slice(-1)]} - {/if} -
- -
- {#if !isJoker(card) && card.slice(0, -1) === "J"} -
J: "{state.player.tech_jack}"
- {:else if !isJoker(card) && card.slice(0, -1) === "Q"} -
Q: "{state.player.tech_queen}"
- {:else if !isJoker(card) && card.slice(0, -1) === "K"} -
K: "{state.player.tech_king}"
- {/if} -
- -
- {isJoker(card) ? '🃏' : card.slice(0, -1)} - {isJoker(card) ? '' : {'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[card.slice(-1)]} -
-
+ { + e.dataTransfer.setData('text/plain', card); + e.currentTarget.classList.add("dragging"); + }} + on:dragend={(e) => { + e.currentTarget.classList.remove("dragging"); + }} /> {:else}

Your hand is empty! (You draw cards when playing cards that match the suit color of the obstacle.)

{/each}
- {#if state.player.role !== "deep"} -
-

🐀 {state.player.name}'s Character Sheet

- -
- {#if state.player.is_dead} -
-

💀 You have Died / Retired!

-

Your story arc is complete. You will choose to become a Ghost or roll a new character during the upkeep phase between scenes.

-
- {/if} - - {#if state.player.is_ghost} -
-

👻 Playing as a Ghost

-

You are in the Ghost World (grayscale view). You cannot interact well with the living but you can still help them resolve challenges!

-
- {/if} - - {#if state.player.needs_name} -
-

🏴‍☠️ You Earned a Name!

-

You completed your second objective and ranked up! What is your new Pirate Name?

-
- - -
-
- {/if} - - {#if state.player.tax_banned} -
-

🚫 You failed a refused Tax — no more Gat/Name Taxes for you this scene.

-
- {/if} - -
-

Description:

-

Look: {state.player.avatar_look}

-

Smell: {state.player.avatar_smell}

-

First Words: "{state.player.first_words}"

-
- -
-

Assigned Secret Techniques (J/Q/K):

-
    -
  • Jack (J): "{state.player.tech_jack}"
  • -
  • Queen (Q): "{state.player.tech_queen}"
  • -
  • King (K): "{state.player.tech_king}"
  • -
-
- - - {#if !state.player.is_dead && scenePirats.filter(p => p.id !== state.player.id && !p.is_dead).length > 0} -
-

⚔️ Duel a Pi-Rat

-

Challenge a crewmate (e.g. for the Captaincy)! Your card becomes a temporary Obstacle they must beat. Both cards are discarded afterwards.

- - - -
- {/if} - -
-

Crew Objectives

-
- - - -
-
- -
-

Personal Objectives

-
- - - -
-
-
-
+ {/if} - -
- - - {#if showEventLog} -
- {#each state.events as event} -
- {event.message} -
- {:else} -
No events yet.
- {/each} -
- {/if} -
- - + diff --git a/frontend/src/components/UpkeepPhase.svelte b/frontend/src/components/UpkeepPhase.svelte index 4e999bd..bcd3c1c 100644 --- a/frontend/src/components/UpkeepPhase.svelte +++ b/frontend/src/components/UpkeepPhase.svelte @@ -1,6 +1,7 @@ + +{#if error} +
{error}
+{/if} + +{#each openChallenges as ch} + {@const chPlays = JSON.parse(ch.plays || '[]')} + {@const chObstacleIds = JSON.parse(ch.obstacle_ids || '[]')} +
+
+

+ {#if ch.challenge_type === 'pvp'} + ⚔️ Duel: {playerName(ch.challenger_player_id)} vs {playerName(ch.target_player_id)} + {:else} + ⚔️ The Deep challenges {playerName(ch.target_player_id)}! + {/if} +

+ {#if state.player.role === 'deep' && ch.challenge_type === 'deep'} +
+ + +
+ {/if} +
+ {#if ch.stakes} +

Stakes: {ch.stakes}

+ {/if} + {#if ch.challenge_type === 'pvp'} +

+ Temporary Obstacle: {getCardDisplay(ch.temp_card)} — {playerName(ch.acting_player_id)} must answer it! +

+ {:else} +

+ Applied Obstacles: {chObstacleIds.map(oid => state.obstacles.find(o => o.id === oid)?.title || '(discarded)').join(', ') || 'None left'} + — attempted by {playerName(ch.acting_player_id)}{ch.acting_player_id !== ch.target_player_id ? ` (took it over via ${ch.tax_type === 'gat' ? 'Gat' : 'Name'} Tax)` : ''}. + Other Pi-Rats may assist (assistants don't draw back). +

+ {/if} + {#if chPlays.length > 0} +

+ Plays: {chPlays.map(p => `${p.player_name}: ${getCardDisplay(p.card)} ${p.success ? '✔' : '✘'}`).join(' · ')} +

+ {/if} + + + {#if ch.tax_state === 'requested'} + {#if myTaxRequest && myTaxRequest.id === ch.id} +
+

{playerName(ch.target_player_id)} calls a {ch.tax_type === 'gat' ? 'Gat' : 'Name'} Tax on you: take this Challenge for them!

+

Accept: you get one random card from their hand and attempt the Challenge. Refuse: you hand over your {ch.tax_type === 'gat' ? 'Gat' : 'Name'} — they keep it if they succeed!

+
+ + +
+
+ {:else} +

⏳ Waiting for {playerName(ch.tax_target_id)} to answer the {ch.tax_type === 'gat' ? 'Gat' : 'Name'} Tax...

+ {/if} + {/if} + + + {#if ch.challenge_type === 'deep' && ch.acting_player_id === state.player.id && !ch.tax_state && !state.player.tax_banned && taxType(state.player) && eligibleTaxTargets().length > 0} +
+ No {taxType(state.player)}? Make it someone else's problem: + + +
+ {/if} + + + {#if myPvpDefense && myPvpDefense.id === ch.id} +
+

Defend yourself! Pick a card:

+
+ {#each hand.filter(c => !isJoker(c)) as card} + + {/each} +
+
+ {/if} +
+{/each} diff --git a/frontend/src/components/scene/CharacterSheet.svelte b/frontend/src/components/scene/CharacterSheet.svelte new file mode 100644 index 0000000..8d902f8 --- /dev/null +++ b/frontend/src/components/scene/CharacterSheet.svelte @@ -0,0 +1,156 @@ + + +
+

🐀 {state.player.name}'s Character Sheet

+ +
+ {#if error} +
{error}
+ {/if} + + {#if state.player.is_dead} +
+

💀 You have Died / Retired!

+

Your story arc is complete. You will choose to become a Ghost or roll a new character during the upkeep phase between scenes.

+
+ {/if} + + {#if state.player.is_ghost} +
+

👻 Playing as a Ghost

+

You are in the Ghost World (grayscale view). You cannot interact well with the living but you can still help them resolve challenges!

+
+ {/if} + + {#if state.player.needs_name} +
+

🏴‍☠️ You Earned a Name!

+

You completed your second objective and ranked up! What is your new Pirate Name?

+
+ + +
+
+ {/if} + + {#if state.player.tax_banned} +
+

🚫 You failed a refused Tax — no more Gat/Name Taxes for you this scene.

+
+ {/if} + +
+

Description:

+

Look: {state.player.avatar_look}

+

Smell: {state.player.avatar_smell}

+

First Words: "{state.player.first_words}"

+
+ +
+

Assigned Secret Techniques (J/Q/K):

+
    +
  • Jack (J): "{state.player.tech_jack}"
  • +
  • Queen (Q): "{state.player.tech_queen}"
  • +
  • King (K): "{state.player.tech_king}"
  • +
+
+ + + {#if !state.player.is_dead && duelTargets.length > 0} +
+

⚔️ Duel a Pi-Rat

+

Challenge a crewmate (e.g. for the Captaincy)! Your card becomes a temporary Obstacle they must beat. Both cards are discarded afterwards.

+ + + +
+ {/if} + +
+

Crew Objectives

+
+ + + +
+
+ +
+

Personal Objectives

+
+ + + +
+
+
+
diff --git a/frontend/src/components/scene/DeepControlPanel.svelte b/frontend/src/components/scene/DeepControlPanel.svelte new file mode 100644 index 0000000..2ec33a3 --- /dev/null +++ b/frontend/src/components/scene/DeepControlPanel.svelte @@ -0,0 +1,152 @@ + + +
+

🌊 Deep Control Panel

+ + {#if error} +
{error}
+ {/if} + + +
+

⚔️ Call a Challenge

+

Establish the stakes, pick a Pi-Rat, and apply one or more Obstacles. They play one card per Obstacle — beating one passes; each failure breeds a complication.

+ +
+ {#each state.obstacles as obs} + {@const taken = challengedObstacleIds.has(obs.id)} + + {/each} +
+ + +
+ + +
+

🏴‍☠️ Captaincy

+

The Captain holds power until they step down, lose a duel, die, or the ship is lost. Reassign after a leadership duel or resignation.

+
+ + +
+
+ +
+

Crew Objectives

+
+ + + +
+
+ +
+

Pi-Rat Personal Objectives

+

You control completion. Only mark in sequence (1 -> 2 -> 3).

+ {#each scenePirats as p} +
+ {p.name} +
+ + + +
+
+ {/each} +
+ + +
+

End the scene once every Pi-Rat has faced a Challenge and had a chance at an Objective, or the Obstacle List is empty.

+ +
+
diff --git a/frontend/src/components/scene/ObstacleBoard.svelte b/frontend/src/components/scene/ObstacleBoard.svelte new file mode 100644 index 0000000..3644355 --- /dev/null +++ b/frontend/src/components/scene/ObstacleBoard.svelte @@ -0,0 +1,131 @@ + + +{#if error} +
{error}
+{/if} + +{#each state.obstacles as obs} + {@const column_cards = JSON.parse(obs.played_cards || '[]')} + {@const active_card_code = column_cards.length > 0 ? column_cards[column_cards.length - 1].card : obs.original_card} + {@const success_count = column_cards.filter(c => c.success).length} + {@const is_completed = success_count >= state.players.length} + {@const in_challenge = challengedObstacleIds.has(obs.id)} +
{ if (!is_completed) e.preventDefault(); }} + on:dragenter={(e) => { if (!is_completed) e.currentTarget.classList.add("drag-over"); }} + on:dragleave={(e) => { if (!is_completed) e.currentTarget.classList.remove("drag-over"); }} + on:drop={(e) => { + if (is_completed) return; + e.preventDefault(); + e.currentTarget.classList.remove("drag-over"); + const cardCode = e.dataTransfer.getData('text/plain'); + if (cardCode) { + if (isJoker(cardCode)) playJoker(obs.id, cardCode); + else playCard(obs.id, cardCode); + } + }}> +
+ +
+
+

{obs.title} {#if in_challenge}⚔️ In Challenge{/if}

+

{obs.description}

+
+ + +
+ Current Difficulty + + {#if ['J', 'Q', 'K'].includes(active_card_code.slice(0, -1))} + {@const rank = challengedRankFor(obs.id)} + {rank !== null ? `${rank} (Rat's Rank)` : "Challenged Rat's Rank"} + {:else} + {obs.current_value} + {/if} + +
+ + +
+ Successes + + {success_count} / {state.players.length} + +
+ + +
+
Card History (Column)
+
+ + {#each column_cards as pc} + + {/each} +
+
+ + {#if is_completed && state.player.role === 'deep'} +
+ +
+ {/if} +
+{:else} +

No active obstacles. Deep players can end the scene!

+{/each} diff --git a/frontend/src/lib/Counter.svelte b/frontend/src/lib/Counter.svelte deleted file mode 100644 index 20bf4c9..0000000 --- a/frontend/src/lib/Counter.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/frontend/src/lib/cards.js b/frontend/src/lib/cards.js new file mode 100644 index 0000000..ffd3f17 --- /dev/null +++ b/frontend/src/lib/cards.js @@ -0,0 +1,27 @@ +// Card-code helpers shared across components. +// Codes look like "10D", "KH", "Joker1" (black) / "Joker2" (red). +export const SUIT_EMOJI = { C: '♣️', S: '♠️', H: '♥️', D: '♦️' }; + +export function isJoker(code) { + return !!code && code.startsWith('Joker'); +} + +// "10D" -> "10" +export function cardValue(code) { + return code.slice(0, -1); +} + +// "10D" -> "D" +export function cardSuit(code) { + return code.slice(-1); +} + +export function getCardDisplay(code) { + if (!code) return ''; + if (isJoker(code)) return '🃏 JOKER'; + return `${cardValue(code)}${SUIT_EMOJI[cardSuit(code)] || cardSuit(code)}`; +} + +export function playerName(players, id) { + return players.find(p => p.id === id)?.name || '???'; +} diff --git a/frontend/src/lib/suggestions.js b/frontend/src/lib/suggestions.js index 12a71f9..c0f9a37 100644 --- a/frontend/src/lib/suggestions.js +++ b/frontend/src/lib/suggestions.js @@ -1,3 +1,5 @@ +import { apiRequest } from './api'; + // Auto-generated math-pirate suggestions pool for character creation const SUGGESTIONS = { "look": [ @@ -1211,227 +1213,42 @@ const SUGGESTIONS = { "They stubbornly argues about the slide rule and has the abacus tattoos to prove it.", "They accidently misplaces the dry gunpowder bags and has a certificate from the Stowaway Academy.", "They frequently drops the slide rule but gets confused by negative numbers." - ], - "techniques": [ - "I meant to do that", - "The floor was always a trap", - "Carlos owes me one", - "Barry already disarmed it", - "Jaspar took the blame", - "Greg always comes back", - "We left Greg on purpose", - "The seagulls work for me", - "My whiskers told me so", - "I was behind you the whole time", - "I am wearing two eyepatches", - "Nobody suspects the small one", - "I licked it, so it's mine", - "The grog made me do it", - "I know a guy", - "The guy knows another guy", - "I greased the entire ship", - "I hid the cannonball in my cheeks", - "I taught the parrot everything it knows", - "Hold my grog", - "Watch this", - "I counted to Pi", - "Pi is exactly three when I'm in a hurry", - "I cast Pie, again", - "I memorized the wrong map on purpose", - "I sneezed at exactly the right moment", - "I read about this in a book I ate", - "I learned this from a ghost", - "The barrel was empty on purpose", - "This is my emotional support cannon", - "That wasn't even my final form", - "I've been holding my breath since Tuesday", - "It's a rental", - "The ship likes me better", - "My cousin is a cannonball", - "My scars spell out a map", - "My other gat is a crossbow", - "I prepared this speech in advance", - "I read the rulebook upside down", - "Technically, that's legal", - "Objection!", - "It's called a heist now", - "You wouldn't hit a rat with glasses", - "These aren't even my real glasses", - "I have the high ground", - "You activated my trap card", - "It was me all along", - "Behold, a distraction", - "We meet again, stairs", - "I've seen this in a dream", - "The prophecy said nothing about Tuesdays", - "Catch!", - "Think fast", - "Look behind you", - "No, seriously, look behind you", - "This one goes to eleven", - "Critical squeak", - "Nothing up my sleeves but more sleeves", - "The anchor was a decoy", - "Six rats in a coat", - "The Deep blinked first", - "The Squid owed me a favor", - "The Albatross vouches for me", - "The Mermaid pinky-swore", - "It worked in the bathtub", - "We rehearsed this exactly once", - "The smell did the negotiating", - "Carry me, I'm dramatic", - "It's only stealing if you get caught", - "Borrowed permanently", - "Never trust a dry deck", - "Never trust a barefoot vampire", - "Never duel before lunch", - "Never sing the second verse", - "Never play cards with a Goblin", - "Never wake a sleeping Bean Whale", - "Never bite the hand that feeds you cheese", - "Never bring a sword to a math fight", - "A rat always has an exit", - "Two exits, actually", - "A wet rat fears no rain", - "A captain never looks up", - "A good plan smells like cheese", - "The loudest rat is rarely the problem", - "Dead men file no complaints", - "The best sword is a longer sword", - "If you can't tie a knot, tie a lot", - "When in doubt, scream and leap", - "Every storm is someone's fault", - "All maps lead to treasure if you're stubborn enough", - "The sea forgives nothing but forgets everything", - "Steal the boat first, the hearts will follow", - "You miss every shot you don't blame on the wind", - "There's no such thing as too much rope", - "It is always squid o'clock somewhere", - "Fortune favors the furry", - "Don't count your doubloons until they're stolen", - "Cheese before glory", - "Glory before breakfast", - "Gravity is a suggestion", - "Rule one: there are no rules", - "Rule two: see rule one", - "The knot unties itself if you believe", - "Smile until they get nervous", - "Just keep nodding", - "Pretend it's Tuesday", - "Speak softly and carry a loud gat", - "Improvise, adapt, overboard", - "Cry first, ask questions later", - "The tide pays its debts", - "Apologize mid-swing", - "Bite first, monologue later", - "Lose dramatically, win quietly", - "Fight like the ghosts are watching", - "Trip over the right thing", - "Be the cannonball", - "Always fall on the soft pirate", - "Flee now, gloat later", - "Dramatic timing is a weapon", - "Cheese knows the way home", - "Trust the mustache", - "The plank is a state of mind", - "Every plank is a trapdoor if you stomp hard enough", - "The pointy end goes in the other guy", - "An apology and a gat beats an apology", - "It's not a bribe, it's a gift", - "The fourth wall is load-bearing", - "Vampires can't resist counting spilled rice", - "Distract them with interpretive dance", - "Sing the shanty of unreasonable confidence", - "The Cheddar Gambit", - "The Gouda Guillotine", - "The Brie Breach", - "The Camembert Cannonade", - "The Reverse Plank Walk", - "The Drunken Compass Defense", - "The Whisker Feint", - "The Triple Barrel Roll", - "The Stowaway Shuffle", - "The Bilge Rat Backflip", - "The Captain's Distraction Waltz", - "The Forbidden Knot", - "The Unforgivable Cannonball", - "The Polite Mutiny", - "The Reverse Mutiny", - "The Squeak of a Thousand Regrets", - "The Tail-First Retreat", - "The Upside-Down Boarding Party", - "The Invisible Rope Trick", - "The Two-Hat Bamboozle", - "The Ghost Pepper Defense", - "The Barnacle Embrace", - "The Crow's Nest Cannonball", - "The Rigging Tango", - "The Anchor Yo-Yo", - "The Powder Keg Lullaby", - "The Mermaid's IOU", - "The Goblin Refund Policy", - "The Fifth Ace", - "The Sixth Ace", - "The Emergency Wedding", - "The Slow-Motion Walk Away", - "The Borrowed Thunder", - "The Cheese Wheel of Fortune", - "The Last Rat Standing", - "The First Rat Running", - "The Old Switcheroo", - "The Old Double Switcheroo", - "The Long Con (Short Version)", - "The Accidental Masterpiece", - "The Sympathy Limp", - "The Decoy Funeral", - "The Surprise Encore", - "The Overly Specific Alibi", - "The Group Hug Ambush", - "The Cannonball Curveball", - "The Soup of Truth", - "The Hat Trick (With Actual Hats)", - "The Thousand-Yard Squint", - "The Completely Legal Salvage", - "Carry the one", - "Round down, shoot up", - "Divide and run away", - "Subtract yourself from the situation", - "Multiply by chaos", - "Add insult to injury", - "The remainder goes in my pocket", - "Long division, short fuse", - "Triangulate, then detonate", - "Roll for cheese", - "Yo ho ho and a barrel of math", - "Ask the cook, he's seen worse", - "Barry has a boat for this", - "Tell Jaspar I said hello", - "The first mate signs my alibis", - "Piss Whiskers takes the watch", - "We voted, you lost", - "The cheese was a lie", - "One more verse won't hurt", - "Abandon ship, but stylishly" ] }; -export function getSuggestion(type, exclude = []) { - let arr = SUGGESTIONS[type]; - if (!arr) return ""; +function pickFrom(arr, exclude = []) { const taken = new Set(exclude.map(s => s.trim().toLowerCase())); const available = arr.filter(s => !taken.has(s.trim().toLowerCase())); - if (available.length > 0) arr = available; - return arr[Math.floor(Math.random() * arr.length)]; + const pool = available.length > 0 ? available : arr; + return pool[Math.floor(Math.random() * pool.length)]; } -// Draws `count` distinct suggestions from a pool. -export function getSuggestions(type, count) { +export function getSuggestion(type, exclude = []) { + const arr = SUGGESTIONS[type]; + if (!arr) return ""; + return pickFrom(arr, exclude); +} + +// Secret Pirate Technique names live in the backend (it also uses them to equip +// re-rolled recruits); fetch the pool once and pick from it client-side. +let techniquePoolPromise = null; +function loadTechniquePool() { + if (!techniquePoolPromise) { + techniquePoolPromise = apiRequest('/suggestions/techniques').then(d => d.techniques); + } + return techniquePoolPromise; +} + +export async function getTechniqueSuggestion(exclude = []) { + return pickFrom(await loadTechniquePool(), exclude); +} + +// Draws `count` distinct technique suggestions. +export async function getTechniqueSuggestions(count, exclude = []) { + const pool = await loadTechniquePool(); const picked = []; for (let i = 0; i < count; i++) { - const s = getSuggestion(type, picked); - if (!s) break; - picked.push(s); + picked.push(pickFrom(pool, [...exclude, ...picked])); } return picked; } diff --git a/src/pirats/cards.py b/src/pirats/cards.py index ea72a24..c3b3a88 100644 --- a/src/pirats/cards.py +++ b/src/pirats/cards.py @@ -1,5 +1,5 @@ import random -from typing import Dict, Any, List, Tuple +from typing import Dict, Any, List SUITS = { "C": {"symbol": "♣", "name": "Clubs", "color": "black", "narration": "Shootin' and Stabbin' (violence/combat)"}, @@ -74,50 +74,209 @@ OBSTACLES_DATA = { } } -# Secret Pirate Technique suggestions for when players need ideas: 20 handcrafted -# names plus the same "{math word} {move} of {target}" combinations the frontend -# suggestion pool (frontend/src/lib/suggestions.js) is built from. -_HANDCRAFTED_TECHNIQUES = [ - "I missed on purpose", - "Carlos will figure it out", - "Never trust a hatless captain", - "I'm not left handed", - "The Albatross is always watching", - "I've got a bomb", - "Divide by Pi", - "Cheese-scented smoke screen", - "Look behind you, a three-headed parrot!", - "Parabolic boarding leap", - "The Pocket Cannon", - "Aggressive Math-whining", - "Unspeakable Tail Swish", - "Bandana Whip", - "Double-barrelled cheese blaster", - "Mathematical Scurvy cure", - "I was hiding under the carpet", - "Batten down my feelings", - "Wait, is that cheese?", - "Sudden pirate flip" -] - -_TECHNIQUE_MATH_WORDS = [ - "Geometric", "Parabolic", "Fibonacci", "Trigonometric", "Prime Number", - "Logarithmic", "Hypotenuse", "Algebraic", "Binary", "Vector" -] -_TECHNIQUE_MOVES = [ - "Drift", "Leap", "Flurry", "Slash", "Parry", - "Slam", "Dodge", "Vortex", "Shield", "Barrage" -] -_TECHNIQUE_TARGETS = [ - "the Cheese Reef", "the Stormy Sea", "Euler", "the Abacus", "the Gat", - "Carlos", "the Deep", "Gauss", "the Coordinate Plane", "Shrapnel" -] - -TECHNIQUE_SUGGESTIONS = _HANDCRAFTED_TECHNIQUES + [ - f"{math_word} {move} of {target}" - for math_word in _TECHNIQUE_MATH_WORDS - for move in _TECHNIQUE_MOVES - for target in _TECHNIQUE_TARGETS +# Secret Pirate Technique suggestions: served to the frontend's Suggest buttons +# via GET /api/suggestions/techniques and used to equip re-rolled recruits. +TECHNIQUE_SUGGESTIONS = [ + "I meant to do that", + "The floor was always a trap", + "Carlos owes me one", + "Barry already disarmed it", + "Jaspar took the blame", + "Greg always comes back", + "We left Greg on purpose", + "The seagulls work for me", + "My whiskers told me so", + "I was behind you the whole time", + "I am wearing two eyepatches", + "Nobody suspects the small one", + "I licked it, so it's mine", + "The grog made me do it", + "I know a guy", + "The guy knows another guy", + "I greased the entire ship", + "I hid the cannonball in my cheeks", + "I taught the parrot everything it knows", + "Hold my grog", + "Watch this", + "I counted to Pi", + "Pi is exactly three when I'm in a hurry", + "I cast Pie, again", + "I memorized the wrong map on purpose", + "I sneezed at exactly the right moment", + "I read about this in a book I ate", + "I learned this from a ghost", + "The barrel was empty on purpose", + "This is my emotional support cannon", + "That wasn't even my final form", + "I've been holding my breath since Tuesday", + "It's a rental", + "The ship likes me better", + "My cousin is a cannonball", + "My scars spell out a map", + "My other gat is a crossbow", + "I prepared this speech in advance", + "I read the rulebook upside down", + "Technically, that's legal", + "Objection!", + "It's called a heist now", + "You wouldn't hit a rat with glasses", + "These aren't even my real glasses", + "I have the high ground", + "You activated my trap card", + "It was me all along", + "Behold, a distraction", + "We meet again, stairs", + "I've seen this in a dream", + "The prophecy said nothing about Tuesdays", + "Catch!", + "Think fast", + "Look behind you", + "No, seriously, look behind you", + "This one goes to eleven", + "Critical squeak", + "Nothing up my sleeves but more sleeves", + "The anchor was a decoy", + "Six rats in a coat", + "The Deep blinked first", + "The Squid owed me a favor", + "The Albatross vouches for me", + "The Mermaid pinky-swore", + "It worked in the bathtub", + "We rehearsed this exactly once", + "The smell did the negotiating", + "Carry me, I'm dramatic", + "It's only stealing if you get caught", + "Borrowed permanently", + "Never trust a dry deck", + "Never trust a barefoot vampire", + "Never duel before lunch", + "Never sing the second verse", + "Never play cards with a Goblin", + "Never wake a sleeping Bean Whale", + "Never bite the hand that feeds you cheese", + "Never bring a sword to a math fight", + "A rat always has an exit", + "Two exits, actually", + "A wet rat fears no rain", + "A captain never looks up", + "A good plan smells like cheese", + "The loudest rat is rarely the problem", + "Dead men file no complaints", + "The best sword is a longer sword", + "If you can't tie a knot, tie a lot", + "When in doubt, scream and leap", + "Every storm is someone's fault", + "All maps lead to treasure if you're stubborn enough", + "The sea forgives nothing but forgets everything", + "Steal the boat first, the hearts will follow", + "You miss every shot you don't blame on the wind", + "There's no such thing as too much rope", + "It is always squid o'clock somewhere", + "Fortune favors the furry", + "Don't count your doubloons until they're stolen", + "Cheese before glory", + "Glory before breakfast", + "Gravity is a suggestion", + "Rule one: there are no rules", + "Rule two: see rule one", + "The knot unties itself if you believe", + "Smile until they get nervous", + "Just keep nodding", + "Pretend it's Tuesday", + "Speak softly and carry a loud gat", + "Improvise, adapt, overboard", + "Cry first, ask questions later", + "The tide pays its debts", + "Apologize mid-swing", + "Bite first, monologue later", + "Lose dramatically, win quietly", + "Fight like the ghosts are watching", + "Trip over the right thing", + "Be the cannonball", + "Always fall on the soft pirate", + "Flee now, gloat later", + "Dramatic timing is a weapon", + "Cheese knows the way home", + "Trust the mustache", + "The plank is a state of mind", + "Every plank is a trapdoor if you stomp hard enough", + "The pointy end goes in the other guy", + "An apology and a gat beats an apology", + "It's not a bribe, it's a gift", + "The fourth wall is load-bearing", + "Vampires can't resist counting spilled rice", + "Distract them with interpretive dance", + "Sing the shanty of unreasonable confidence", + "The Cheddar Gambit", + "The Gouda Guillotine", + "The Brie Breach", + "The Camembert Cannonade", + "The Reverse Plank Walk", + "The Drunken Compass Defense", + "The Whisker Feint", + "The Triple Barrel Roll", + "The Stowaway Shuffle", + "The Bilge Rat Backflip", + "The Captain's Distraction Waltz", + "The Forbidden Knot", + "The Unforgivable Cannonball", + "The Polite Mutiny", + "The Reverse Mutiny", + "The Squeak of a Thousand Regrets", + "The Tail-First Retreat", + "The Upside-Down Boarding Party", + "The Invisible Rope Trick", + "The Two-Hat Bamboozle", + "The Ghost Pepper Defense", + "The Barnacle Embrace", + "The Crow's Nest Cannonball", + "The Rigging Tango", + "The Anchor Yo-Yo", + "The Powder Keg Lullaby", + "The Mermaid's IOU", + "The Goblin Refund Policy", + "The Fifth Ace", + "The Sixth Ace", + "The Emergency Wedding", + "The Slow-Motion Walk Away", + "The Borrowed Thunder", + "The Cheese Wheel of Fortune", + "The Last Rat Standing", + "The First Rat Running", + "The Old Switcheroo", + "The Old Double Switcheroo", + "The Long Con (Short Version)", + "The Accidental Masterpiece", + "The Sympathy Limp", + "The Decoy Funeral", + "The Surprise Encore", + "The Overly Specific Alibi", + "The Group Hug Ambush", + "The Cannonball Curveball", + "The Soup of Truth", + "The Hat Trick (With Actual Hats)", + "The Thousand-Yard Squint", + "The Completely Legal Salvage", + "Carry the one", + "Round down, shoot up", + "Divide and run away", + "Subtract yourself from the situation", + "Multiply by chaos", + "Add insult to injury", + "The remainder goes in my pocket", + "Long division, short fuse", + "Triangulate, then detonate", + "Roll for cheese", + "Yo ho ho and a barrel of math", + "Ask the cook, he's seen worse", + "Barry has a boat for this", + "Tell Jaspar I said hello", + "The first mate signs my alibis", + "Piss Whiskers takes the watch", + "We voted, you lost", + "The cheese was a lie", + "One more verse won't hurt", + "Abandon ship, but stylishly" ] def get_fresh_deck() -> List[str]: diff --git a/src/pirats/crud_base.py b/src/pirats/crud_base.py index b1f4cb6..cb8c52b 100644 --- a/src/pirats/crud_base.py +++ b/src/pirats/crud_base.py @@ -1,10 +1,12 @@ import json import random -from typing import List, Optional -from sqlmodel import Session -from .models import Game, Player, Obstacle, GameEvent +from typing import Any, Dict, List, Optional +from sqlmodel import Session, select +from .models import Game, Player, GameEvent from . import cards +FACE_VALUES = ("J", "Q", "K") + # --- Card and Hand Utilities --- def get_player_hand(player: Player) -> List[str]: @@ -19,6 +21,64 @@ def get_game_deck(game: Game) -> List[str]: def set_game_deck(game: Game, deck: List[str]): game.deck_cards = json.dumps(deck) +def technique_for_face_card(player: Player, value: str) -> str: + """The Secret Pirate Technique a player has assigned to a face card value (J/Q/K).""" + return { + "J": player.tech_jack or "Jack Secret Technique", + "Q": player.tech_queen or "Queen Secret Technique", + "K": player.tech_king or "King Secret Technique", + }[value] + +def evaluate_card_play( + player: Player, + card_code: str, + obstacle_card_code: str, + obstacle_value: int, + acting_rank: int, +) -> Dict[str, Any]: + """ + Shared Challenge resolution rules for one (non-Joker) card played by a Pi-Rat + (callers must have already rejected Jokers and Deep players): + - J/Q/K triggers the player's assigned Secret Pirate Technique: automatic success. + - A face card acting as the Obstacle is worth the challenged Pi-Rat's Rank + (acting_rank); any other Obstacle card is worth obstacle_value. + - Gat privilege: Black cards +1. Name privilege: Red cards +1. + Returns {"success", "details", "is_technique", "tech_name"}. + """ + played = cards.parse_card(card_code) + + if played["value"] in FACE_VALUES: + tech_name = technique_for_face_card(player, played["value"]) + return { + "success": True, + "details": f"Used Secret Pirate Technique: '{tech_name}'!", + "is_technique": True, + "tech_name": tech_name, + } + + if cards.parse_card(obstacle_card_code)["value"] in FACE_VALUES: + target_value = acting_rank + obs_detail = f"Rank {acting_rank} (Face Card Obstacle)" + else: + target_value = obstacle_value + obs_detail = str(target_value) + + privilege_bonus = 0 + if player.completed_personal_1 and played["suit"] in ("C", "S"): + privilege_bonus += 1 + if player.completed_personal_2 and played["suit"] in ("H", "D"): + privilege_bonus += 1 + final_value = played["numeric_value"] + privilege_bonus + + success = final_value > target_value + outcome = "Success" if success else "Failure" + return { + "success": success, + "details": f"{outcome}! Played {played['display']} ({final_value}) vs Obstacle value {obs_detail}.", + "is_technique": False, + "tech_name": "", + } + def calculate_max_hand_size(player: Player, players_in_scene: List[Player], captain_id: Optional[str] = None) -> int: """ Calculates a player's maximum hand size based on their Rank relative to others, @@ -93,9 +153,9 @@ def set_captain(db: Session, game: Game, player_id: Optional[str], reason: str = msg = f"{new_captain.name} is now the Captain!" if reason: msg += f" ({reason})" - add_game_event(db, game.id, msg) + add_game_event(db, game.id, msg, kind="captain") elif reason: - add_game_event(db, game.id, f"The Captain's position is vacant. ({reason})") + add_game_event(db, game.id, f"The Captain's position is vacant. ({reason})", kind="captain") def reshuffle_discard_pile(db: Session, game: Game): """ @@ -115,6 +175,10 @@ def reshuffle_discard_pile(db: Session, game: Game): played = json.loads(obs.played_cards) for entry in played: active_table_cards.add(entry["card"]) + # A PvP challenger's temporary Obstacle is on the table until the duel resolves + for challenge in game.challenges: + if challenge.status == "open" and challenge.temp_card: + active_table_cards.add(challenge.temp_card) # 3. The full deck of 54 cards full_deck = [] @@ -196,12 +260,30 @@ def add_player(db: Session, game_id: str, name: str, is_creator: bool = False) - db.add(player) db.commit() db.refresh(player) - - add_game_event(db, game_id, f"{name} joined the crew!") - + + # Players joining mid-scene would otherwise never receive cards (starting + # hands are only dealt when scene 1 starts), so deal them in immediately. + if game and game.phase == "scene": + db.refresh(game) + max_size = calculate_max_hand_size(player, game.players, game.captain_player_id) + draw_cards_for_player(db, game, player, max_size) + + add_game_event(db, game_id, f"{name} joined the crew!", kind="join") + return player -def add_game_event(db: Session, game_id: str, message: str): - event = GameEvent(game_id=game_id, message=message) +def add_game_event(db: Session, game_id: str, message: str, kind: str = "info"): + event = GameEvent(game_id=game_id, message=message, kind=kind) db.add(event) db.commit() + +def get_events_page(db: Session, game_id: str, before: Optional[float] = None, limit: int = 50): + """Returns the newest `limit` events older than `before` (newest overall if None), + in ascending timestamp order, plus a flag for whether older events remain.""" + query = select(GameEvent).where(GameEvent.game_id == game_id) + if before is not None: + query = query.where(GameEvent.timestamp < before) + query = query.order_by(GameEvent.timestamp.desc()).limit(limit + 1) + events = db.exec(query).all() + has_more = len(events) > limit + return list(reversed(events[:limit])), has_more diff --git a/src/pirats/crud_challenge.py b/src/pirats/crud_challenge.py index 48d9768..6fb7ec2 100644 --- a/src/pirats/crud_challenge.py +++ b/src/pirats/crud_challenge.py @@ -6,7 +6,7 @@ from .models import Game, Player, Obstacle, Challenge from . import cards from .crud_base import ( get_player, get_game, get_player_hand, set_player_hand, - draw_cards_for_player, add_game_event, change_player_rank + draw_cards_for_player, add_game_event, change_player_rank, evaluate_card_play ) from .crud_scene import resolve_card_against_obstacle @@ -25,7 +25,7 @@ def _apply_captain_tax(db: Session, game: Game, acting_player: Player): """Captains lose 1 Rank each time they personally fail a Challenge.""" if game.captain_player_id == acting_player.id and acting_player.rank > 1: change_player_rank(db, game, acting_player, -1) - add_game_event(db, game.id, f"Captain Tax! {acting_player.name} failed a Challenge and drops to Rank {acting_player.rank}.") + add_game_event(db, game.id, f"Captain Tax! {acting_player.name} failed a Challenge and drops to Rank {acting_player.rank}.", kind="rank") # --- Deep Challenges --- @@ -80,7 +80,7 @@ def create_challenge( msg = f"The Deep challenges {target.name}! Applied: {obstacle_titles}." if stakes.strip(): msg += f" Stakes: {stakes.strip()}" - add_game_event(db, game.id, msg) + add_game_event(db, game.id, msg, kind="challenge") return True, "Challenge called!" def play_challenge_card( @@ -156,7 +156,7 @@ def play_challenge_card( result["details"] += f" (Assisting {acting.name} — assistants don't draw back.)" role_note = "" if player.id == challenge.acting_player_id else " (assist)" - add_game_event(db, game.id, f"{player.name} played {cards.parse_card(card_code)['display']} on '{obstacle.title}'{role_note}. {result['details']}") + add_game_event(db, game.id, f"{player.name} played {cards.parse_card(card_code)['display']} on '{obstacle.title}'{role_note}. {result['details']}", kind="card") result["drew_card"] = drew_card result["is_joker"] = False @@ -191,7 +191,7 @@ def resolve_challenge(db: Session, challenge_id: str, resolver_id: str) -> Tuple db.commit() outcome = "succeeded" if success else "failed" - add_game_event(db, game.id, f"The Challenge against {target.name} {outcome}!") + add_game_event(db, game.id, f"The Challenge against {target.name} {outcome}!", kind="challenge") # Settle a refused Gat/Name Tax: keep the prize on success, return it on failure. if challenge.tax_state == "refused" and challenge.tax_target_id: @@ -199,7 +199,7 @@ def resolve_challenge(db: Session, challenge_id: str, resolver_id: str) -> Tuple thing = "Gat" if challenge.tax_type == "gat" else "Name" if refuser: if success: - add_game_event(db, game.id, f"{acting.name} succeeded and keeps {refuser.name}'s {thing}! {refuser.name} must find a new one.") + add_game_event(db, game.id, f"{acting.name} succeeded and keeps {refuser.name}'s {thing}! {refuser.name} must find a new one.", kind="tax") else: if challenge.tax_type == "gat": acting.completed_personal_1 = False @@ -213,7 +213,7 @@ def resolve_challenge(db: Session, challenge_id: str, resolver_id: str) -> Tuple db.add(acting) db.commit() change_player_rank(db, game, acting, -1) - add_game_event(db, game.id, f"{acting.name} failed and returns the {thing} to {refuser.name}. No more Gat/Name Taxes for {acting.name} this scene!") + add_game_event(db, game.id, f"{acting.name} failed and returns the {thing} to {refuser.name}. No more Gat/Name Taxes for {acting.name} this scene!", kind="tax") if not success: _apply_captain_tax(db, game, acting) @@ -235,7 +235,7 @@ def cancel_challenge(db: Session, challenge_id: str, resolver_id: str) -> Tuple[ db.delete(challenge) db.commit() if game and target: - add_game_event(db, game.id, f"The Deep withdrew the Challenge against {target.name}.") + add_game_event(db, game.id, f"The Deep withdrew the Challenge against {target.name}.", kind="challenge") return True, "Challenge withdrawn." # --- Gat Tax & Name Tax --- @@ -280,7 +280,7 @@ def request_tax(db: Session, challenge_id: str, requester_id: str, tax_target_id game = get_game(db, challenge.game_id) thing = "Gat" if tax_type == "gat" else "Name" - add_game_event(db, game.id, f"{requester.name} calls a {thing} Tax on {target.name}: 'Take this Challenge for me!'") + add_game_event(db, game.id, f"{requester.name} calls a {thing} Tax on {target.name}: 'Take this Challenge for me!'", kind="tax") return True, f"{thing} Tax called on {target.name}." def respond_tax(db: Session, challenge_id: str, responder_id: str, accept: bool) -> Tuple[bool, str]: @@ -324,7 +324,7 @@ def respond_tax(db: Session, challenge_id: str, responder_id: str, accept: bool) challenge.tax_state = "accepted" db.add(challenge) db.commit() - add_game_event(db, game.id, f"{responder.name} accepted the {thing} Tax and takes over the Challenge!{card_note}") + add_game_event(db, game.id, f"{responder.name} accepted the {thing} Tax and takes over the Challenge!{card_note}", kind="tax") return True, "Tax accepted." # Refused: hand over the Gat/Name. The requester attempts the Challenge themselves; @@ -342,7 +342,7 @@ def respond_tax(db: Session, challenge_id: str, responder_id: str, accept: bool) db.add(challenge) db.commit() change_player_rank(db, game, requester, 1) - add_game_event(db, game.id, f"{responder.name} refused the {thing} Tax and must hand over their {thing}! {requester.name} completes that Objective and attempts the Challenge — succeed to keep it!") + add_game_event(db, game.id, f"{responder.name} refused the {thing} Tax and must hand over their {thing}! {requester.name} completes that Objective and attempts the Challenge — succeed to keep it!", kind="tax") return True, "Tax refused. The prize is on the line!" # --- Pi-Rat vs Pi-Rat Challenges --- @@ -397,7 +397,7 @@ def create_pvp_challenge( parsed = cards.parse_card(card_code) narration = cards.SUITS[parsed["suit"]]["narration"] - add_game_event(db, game.id, f"{challenger.name} challenges {defender.name}, playing {parsed['display']} as a temporary Obstacle — {narration}!") + add_game_event(db, game.id, f"{challenger.name} challenges {defender.name}, playing {parsed['display']} as a temporary Obstacle — {narration}!", kind="challenge") return True, "Duel called!" def play_pvp_defense(db: Session, challenge_id: str, defender_id: str, card_code: str) -> Tuple[bool, str, Dict[str, Any]]: @@ -430,43 +430,10 @@ def play_pvp_defense(db: Session, challenge_id: str, defender_id: str, card_code set_player_hand(defender, hand) db.add(defender) - temp_parsed = cards.parse_card(challenge.temp_card) - - success = False - details = "" - is_technique = False - tech_name = "" - - if played_parsed["value"] in ["J", "Q", "K"]: - success = True - is_technique = True - tech_name = { - "J": defender.tech_jack or "Jack Secret Technique", - "Q": defender.tech_queen or "Queen Secret Technique", - "K": defender.tech_king or "King Secret Technique", - }[played_parsed["value"]] - details = f"Used Secret Pirate Technique: '{tech_name}'!" - else: - # A face card acting as an Obstacle is worth the challenged Pi-Rat's Rank - if temp_parsed["value"] in ["J", "Q", "K"]: - target_value = defender.rank - obs_detail = f"Rank {defender.rank} (Face Card Obstacle)" - else: - target_value = temp_parsed["numeric_value"] - obs_detail = str(target_value) - - privilege_bonus = 0 - if defender.completed_personal_1 and played_parsed["suit"] in ["C", "S"]: - privilege_bonus = 1 - if defender.completed_personal_2 and played_parsed["suit"] in ["H", "D"]: - privilege_bonus = 1 - final_value = played_parsed["numeric_value"] + privilege_bonus - - if final_value > target_value: - success = True - details = f"Success! Played {played_parsed['display']} ({final_value}) vs {temp_parsed['display']} ({obs_detail})." - else: - details = f"Failure! Played {played_parsed['display']} ({final_value}) vs {temp_parsed['display']} ({obs_detail})." + # The temp card is the Obstacle: both its value source and its face-card check. + temp_value = cards.parse_card(challenge.temp_card)["numeric_value"] + result = evaluate_card_play(defender, card_code, challenge.temp_card, temp_value, defender.rank) + success = result["success"] plays = json.loads(challenge.plays) plays.append({ @@ -475,7 +442,7 @@ def play_pvp_defense(db: Session, challenge_id: str, defender_id: str, card_code "player_id": defender.id, "player_name": defender.name, "success": success, - "details": details + "details": result["details"] }) challenge.plays = json.dumps(plays) challenge.status = "succeeded" if success else "failed" @@ -487,19 +454,14 @@ def play_pvp_defense(db: Session, challenge_id: str, defender_id: str, card_code drawn = draw_cards_for_player(db, game, defender, 1) if drawn: drew_card = drawn[0] - details += " (Drew a card back due to matching suit colors!)" + result["details"] += " (Drew a card back due to matching suit colors!)" outcome = "wins" if success else "loses" - add_game_event(db, game.id, f"{defender.name} defends against {challenger.name} and {outcome}! {details}") + add_game_event(db, game.id, f"{defender.name} defends against {challenger.name} and {outcome}! {result['details']}", kind="challenge") if not success: _apply_captain_tax(db, game, defender) - return True, "Duel resolved.", { - "success": success, - "details": details, - "is_technique": is_technique, - "tech_name": tech_name, - "drew_card": drew_card, - "is_joker": False - } + result["drew_card"] = drew_card + result["is_joker"] = False + return True, "Duel resolved.", result diff --git a/src/pirats/crud_character.py b/src/pirats/crud_character.py index 4ec7a8f..dd1d01d 100644 --- a/src/pirats/crud_character.py +++ b/src/pirats/crud_character.py @@ -1,7 +1,7 @@ import json import random from sqlmodel import Session -from .models import Game, Player +from .models import Game from .crud_base import get_player, get_game, add_game_event # --- Character Creation Operations --- @@ -126,7 +126,7 @@ def trigger_technique_swap(db: Session, game: Game): p.created_techniques = "[]" db.add(p) db.commit() - add_game_event(db, game.id, "Duplicate techniques submitted! The pool has been cleared. All players must try again with unique technique names.") + add_game_event(db, game.id, "Duplicate techniques submitted! The pool has been cleared. All players must try again with unique technique names.", kind="warning") return # Swap algorithm with simple retry backtracking @@ -166,7 +166,7 @@ def trigger_technique_swap(db: Session, game: Game): game.phase = "swap_techniques" db.add(game) db.commit() - add_game_event(db, game.id, "Phase changed: Swap Techniques") + add_game_event(db, game.id, "Phase changed: Swap Techniques", kind="phase") success = True break @@ -180,7 +180,7 @@ def trigger_technique_swap(db: Session, game: Game): game.phase = "swap_techniques" db.add(game) db.commit() - add_game_event(db, game.id, "Phase changed: Swap Techniques") + add_game_event(db, game.id, "Phase changed: Swap Techniques", kind="phase") def assign_face_card_techniques(db: Session, player_id: str, jack: str, queen: str, king: str): player = get_player(db, player_id) @@ -234,7 +234,7 @@ def assign_face_card_techniques(db: Session, player_id: str, jack: str, queen: s game.phase = "scene_setup" db.add(game) db.commit() - add_game_event(db, game.id, "Phase changed: Scene Setup") + add_game_event(db, game.id, "Phase changed: Scene Setup", kind="phase") def roll_new_character( db: Session, @@ -347,4 +347,4 @@ def roll_new_character( max_size = calculate_max_hand_size(player, game.players, game.captain_player_id) draw_cards_for_player(db, game, player, max_size) - add_game_event(db, game.id, f"{player.name} has joined the crew as a new Rank {player.rank} recruit!") + add_game_event(db, game.id, f"{player.name} has joined the crew as a new Rank {player.rank} recruit!", kind="join") diff --git a/src/pirats/crud_scene.py b/src/pirats/crud_scene.py index edf579d..226253e 100644 --- a/src/pirats/crud_scene.py +++ b/src/pirats/crud_scene.py @@ -1,14 +1,13 @@ import json import random -from typing import Tuple, Dict, Any, Optional +from typing import Tuple, Dict, Any from sqlmodel import Session -from .models import Game, Player, Obstacle, Challenge +from .models import Game, Player, Obstacle from . import cards from .crud_base import ( get_player, get_game, get_player_hand, set_player_hand, - get_game_deck, set_game_deck, calculate_max_hand_size, draw_cards_for_player, - add_game_event, reshuffle_discard_pile, capture_hand_maxes, apply_hand_increases, - change_player_rank, set_captain + get_game_deck, calculate_max_hand_size, add_game_event, reshuffle_discard_pile, + change_player_rank, set_captain, evaluate_card_play ) # --- Scene Setup Operations --- @@ -115,7 +114,7 @@ def confirm_scene_setup(db: Session, game_id: str) -> Tuple[bool, str]: # Joker drawn as an Obstacle: discard it and permanently increase the # number of Obstacles required for following scenes by 1. game.extra_obstacles += 1 - add_game_event(db, game.id, "A Joker surfaced as an Obstacle! Future scenes permanently require one more Obstacle.") + add_game_event(db, game.id, "A Joker surfaced as an Obstacle! Future scenes permanently require one more Obstacle.", kind="joker") continue info = cards.get_obstacle_info(card) @@ -148,7 +147,7 @@ def confirm_scene_setup(db: Session, game_id: str) -> Tuple[bool, str]: db.commit() total_obstacles = active_count + drawn_count - add_game_event(db, game.id, f"Scene {game.current_scene_number} has started! {drawn_count} new obstacle(s) drawn ({total_obstacles} active).") + add_game_event(db, game.id, f"Scene {game.current_scene_number} has started! {drawn_count} new obstacle(s) drawn ({total_obstacles} active).", kind="scene") return True, "Scene started successfully!" @@ -193,83 +192,30 @@ def resolve_card_against_obstacle( acting_rank: int ) -> Dict[str, Any]: """ - Resolves one (non-Joker) card from a player against an Obstacle: - determines success (Secret Pirate Techniques, Gat/Name privileges, face-card - Obstacle values), appends the card to the Obstacle's column, and updates - the Obstacle's current value. Does not touch hands or draws. + Resolves one (non-Joker) card from a player against an Obstacle via the shared + rules in evaluate_card_play, appends the card to the Obstacle's column, and + updates the Obstacle's current value. Does not touch hands or draws. """ - played_parsed = cards.parse_card(card_code) + # The last card in the column (or the original Obstacle card) determines the value. + played_list = json.loads(obstacle.played_cards) + obstacle_card = played_list[-1]["card"] if played_list else obstacle.original_card - success = False - details = "" - is_technique = False - tech_name = "" - - # Face Cards (Jack, Queen, King) played by a Pi-Rat trigger a Secret Pirate Technique: automatic success! - if played_parsed["value"] in ["J", "Q", "K"] and player.role != "deep": - success = True - is_technique = True - if played_parsed["value"] == "J": - tech_name = player.tech_jack or "Jack Secret Technique" - elif played_parsed["value"] == "Q": - tech_name = player.tech_queen or "Queen Secret Technique" - elif played_parsed["value"] == "K": - tech_name = player.tech_king or "King Secret Technique" - details = f"Used Secret Pirate Technique: '{tech_name}'!" - else: - # Regular card play vs Obstacle value. The last card in the column (or the - # original Obstacle card) determines the value; face cards count as the - # Rank of the challenged Pi-Rat. - obs_val_card = cards.parse_card(obstacle.original_card) - played_on_obs = json.loads(obstacle.played_cards) - if played_on_obs: - obs_val_card = cards.parse_card(played_on_obs[-1]["card"]) - - if obs_val_card["value"] in ["J", "Q", "K"]: - target_value = acting_rank - obs_detail = f"Rank {acting_rank} (Face Card Obstacle)" - else: - target_value = obstacle.current_value - obs_detail = str(target_value) - - played_val = played_parsed["numeric_value"] - - # Gat privilege: Black cards +1. Name privilege: Red cards +1. - privilege_bonus = 0 - if player.completed_personal_1 and played_parsed["suit"] in ["C", "S"]: - privilege_bonus = 1 - if player.completed_personal_2 and played_parsed["suit"] in ["H", "D"]: - privilege_bonus = 1 - - final_played_value = played_val + privilege_bonus - - if final_played_value > target_value: - success = True - details = f"Success! Played {played_parsed['display']} ({final_played_value}) vs Obstacle value {obs_detail}." - else: - success = False - details = f"Failure! Played {played_parsed['display']} ({final_played_value}) vs Obstacle value {obs_detail}." + result = evaluate_card_play(player, card_code, obstacle_card, obstacle.current_value, acting_rank) # Place the card in the Obstacle's column; it becomes the Obstacle's new value. - played_list = json.loads(obstacle.played_cards) played_list.append({ "card": card_code, "player_id": player.id, "player_name": player.name, - "success": success, - "details": details + "success": result["success"], + "details": result["details"] }) obstacle.played_cards = json.dumps(played_list) - obstacle.current_value = played_parsed["numeric_value"] + obstacle.current_value = cards.parse_card(card_code)["numeric_value"] db.add(obstacle) db.commit() - return { - "success": success, - "details": details, - "is_technique": is_technique, - "tech_name": tech_name - } + return result def play_joker(db: Session, player_id: str, obstacle_id: str, card_code: str) -> Tuple[bool, str]: """ @@ -318,7 +264,7 @@ def play_joker(db: Session, player_id: str, obstacle_id: str, card_code: str) -> new_parsed = cards.parse_card(new_card) if new_parsed["is_joker"]: game.extra_obstacles += 1 - add_game_event(db, game.id, "A Joker surfaced as an Obstacle! Future scenes permanently require one more Obstacle.") + add_game_event(db, game.id, "A Joker surfaced as an Obstacle! Future scenes permanently require one more Obstacle.", kind="joker") continue info = cards.get_obstacle_info(new_card) @@ -340,7 +286,7 @@ def play_joker(db: Session, player_id: str, obstacle_id: str, card_code: str) -> msg = f"{player.name} played a Joker! '{obstacle_title}' is discarded" msg += f" and replaced by '{new_title}'." if new_title else " (no replacement left in the deck)." - add_game_event(db, game.id, msg) + add_game_event(db, game.id, msg, kind="joker") return True, msg @@ -418,7 +364,7 @@ def toggle_objective(db: Session, game_id: str, target_id: str, obj_type: str, s obj_name = obj_type.replace('_', ' ').title() status_text = "completed" if status else "unmarked" - add_game_event(db, game_id, f"{player.name} marked objective '{obj_name}' as {status_text}.") + add_game_event(db, game_id, f"{player.name} marked objective '{obj_name}' as {status_text}.", kind="objective") def rollback_card_play(db: Session, obstacle_id: str) -> Tuple[bool, str]: obstacle = db.get(Obstacle, obstacle_id) @@ -471,7 +417,7 @@ def rollback_card_play(db: Session, obstacle_id: str) -> Tuple[bool, str]: def clear_completed_obstacle(db: Session, game_id: str, obstacle_id: str): obstacle = db.get(Obstacle, obstacle_id) if obstacle: - add_game_event(db, game_id, f"The Deep cleared the completed obstacle '{obstacle.title}'.") + add_game_event(db, game_id, f"The Deep cleared the completed obstacle '{obstacle.title}'.", kind="obstacle") db.delete(obstacle) db.commit() @@ -483,4 +429,4 @@ def finish_game(db: Session, game_id: str): game.phase = "ended" db.add(game) db.commit() - add_game_event(db, game_id, "The story has ended! The Pi-Rats party til they pass out in a pile. 🎉") + add_game_event(db, game_id, "The story has ended! The Pi-Rats party til they pass out in a pile. 🎉", kind="victory") diff --git a/src/pirats/crud_upkeep.py b/src/pirats/crud_upkeep.py index 2c998ae..9c81f88 100644 --- a/src/pirats/crud_upkeep.py +++ b/src/pirats/crud_upkeep.py @@ -1,8 +1,7 @@ -import json import random from typing import List, Tuple from sqlmodel import Session, select -from .models import Game, Player, Vote +from .models import Game, Vote from .crud_base import ( get_player, get_game, get_player_hand, set_player_hand, get_game_deck, set_game_deck, draw_cards_for_player, add_game_event, @@ -56,7 +55,7 @@ def end_scene_and_transition(db: Session, game_id: str): db.add(game) db.commit() - add_game_event(db, game_id, f"Scene {game.current_scene_number} has ended! Transitioning to upkeep phase.") + add_game_event(db, game_id, f"Scene {game.current_scene_number} has ended! Transitioning to upkeep phase.", kind="scene") def process_between_scenes_votes(db: Session, game: Game): """ @@ -82,7 +81,7 @@ def process_between_scenes_votes(db: Session, game: Game): winner = get_player(db, winner_id) if winner: change_player_rank(db, game, winner, 1) - add_game_event(db, game.id, f"{winner.name} was voted to rank up! They are now Rank {winner.rank}.") + add_game_event(db, game.id, f"{winner.name} was voted to rank up! They are now Rank {winner.rank}.", kind="rank") # Clear all votes for v in votes: @@ -107,13 +106,13 @@ def transition_to_deep_upkeep(db: Session, game_id: str): if has_resting_deep: game.phase = "deep_upkeep" - add_game_event(db, game.id, "Phase changed: Deep Upkeep") + add_game_event(db, game.id, "Phase changed: Deep Upkeep", kind="phase") else: # Fallback in case there were no Deep players # Just advance directly to scene_setup game.current_scene_number += 1 game.phase = "scene_setup" - add_game_event(db, game.id, "Phase changed: Scene Setup") + add_game_event(db, game.id, "Phase changed: Scene Setup", kind="phase") for p in game.players: p.role = None p.is_ready = False @@ -164,7 +163,7 @@ def confirm_deep_refresh(db: Session, player_id: str, discard_cards: List[str]): # Transition to scene_setup! game.current_scene_number += 1 game.phase = "scene_setup" - add_game_event(db, game.id, "Phase changed: Scene Setup") + add_game_event(db, game.id, "Phase changed: Scene Setup", kind="phase") for p in game.players: p.is_ready = False p.role = None diff --git a/src/pirats/database.py b/src/pirats/database.py index da46816..bced88e 100644 --- a/src/pirats/database.py +++ b/src/pirats/database.py @@ -31,6 +31,7 @@ def create_db_and_tables(): ("player", "is_dead", "BOOLEAN DEFAULT FALSE"), ("player", "is_ghost", "BOOLEAN DEFAULT FALSE"), ("player", "tax_banned", "BOOLEAN DEFAULT FALSE"), + ("gameevent", "kind", "VARCHAR DEFAULT 'info'"), ] for table, col, def_type in columns: diff --git a/src/pirats/main.py b/src/pirats/main.py index 098c3b6..ae71cbb 100644 --- a/src/pirats/main.py +++ b/src/pirats/main.py @@ -1,25 +1,26 @@ +from contextlib import asynccontextmanager from typing import Optional -from fastapi import FastAPI, Request, Form, Depends, HTTPException, status, APIRouter -from fastapi.responses import HTMLResponse, JSONResponse +from fastapi import FastAPI, Form, Depends, HTTPException, APIRouter from fastapi.staticfiles import StaticFiles -from sqlmodel import Session, select +from sqlmodel import Session import uvicorn import os from pathlib import Path from . import crud from .database import create_db_and_tables, get_session -from .models import GameEvent + +EVENT_PAGE_SIZE = 50 BASE_DIR = Path(__file__).parent -app = FastAPI(title="Rats with Gats Remote Play API") -api = APIRouter() - -# Register startup event to create tables -@app.on_event("startup") -def on_startup(): +@asynccontextmanager +async def lifespan(app: FastAPI): create_db_and_tables() + yield + +app = FastAPI(title="Rats with Gats Remote Play API", lifespan=lifespan) +api = APIRouter() # --- API Core Route Handlers --- @@ -29,7 +30,8 @@ def create_game_route( db: Session = Depends(get_session) ): game = crud.create_game(db, crew_name=crew_name.strip()) - return {"id": game.id} + # admin_key is only revealed here, to the creator; the state endpoint hides it + return {"id": game.id, "admin_key": game.admin_key} @api.post("/game/{game_id}/join") def join_game_submit( @@ -53,16 +55,36 @@ def get_game_state(game_id: str, player_id: str, db: Session = Depends(get_sessi if not game or not player: raise HTTPException(status_code=404, detail="Game or Player not found") - events = db.exec(select(GameEvent).where(GameEvent.game_id == game_id).order_by(GameEvent.timestamp.asc())).all() - + events, events_have_more = crud.get_events_page(db, game_id, limit=EVENT_PAGE_SIZE) + return { - "game": game.model_dump(), + "game": game.model_dump(exclude={"admin_key"}), "player": player.model_dump(), "players": [p.model_dump() for p in game.players], "obstacles": [o.model_dump() for o in game.obstacles], "challenges": [c.model_dump() for c in game.challenges], "votes": [v.model_dump() for v in game.votes], "events": [e.model_dump() for e in events], + "events_have_more": events_have_more, + } + +@api.get("/game/{game_id}/events") +def get_game_events( + game_id: str, + before: Optional[float] = None, + limit: int = EVENT_PAGE_SIZE, + db: Session = Depends(get_session), +): + """Pages back through the event log: returns the newest `limit` events older than `before`.""" + game = crud.get_game(db, game_id) + if not game: + raise HTTPException(status_code=404, detail="Game not found") + + limit = max(1, min(limit, 200)) + events, have_more = crud.get_events_page(db, game_id, before=before, limit=limit) + return { + "events": [e.model_dump() for e in events], + "have_more": have_more, } # --- Register Modular Phase Routers --- diff --git a/src/pirats/models.py b/src/pirats/models.py index a6e810a..0f9667a 100644 --- a/src/pirats/models.py +++ b/src/pirats/models.py @@ -121,5 +121,6 @@ class GameEvent(SQLModel, table=True): game_id: str = Field(foreign_key="game.id", index=True) timestamp: float = Field(default_factory=time.time) message: str = Field(...) + kind: str = Field(default="info") # category for frontend icons/styling: "join", "phase", "scene", "captain", "challenge", "card", "tax", "objective", "vote", "obstacle", "joker", "victory", "warning", "rank", "info" game: Optional[Game] = Relationship(back_populates="events") diff --git a/src/pirats/routes_admin.py b/src/pirats/routes_admin.py index 4a96c8d..acff550 100644 --- a/src/pirats/routes_admin.py +++ b/src/pirats/routes_admin.py @@ -1,5 +1,4 @@ from fastapi import APIRouter, Request, Depends, HTTPException -from fastapi.responses import JSONResponse from sqlmodel import Session from .database import get_session from . import crud diff --git a/src/pirats/routes_character.py b/src/pirats/routes_character.py index 0bee3a8..eb7abe6 100644 --- a/src/pirats/routes_character.py +++ b/src/pirats/routes_character.py @@ -1,12 +1,17 @@ -from typing import Optional -from fastapi import APIRouter, Form, Depends, HTTPException, status +from fastapi import APIRouter, Form, Depends, HTTPException from fastapi.responses import JSONResponse from sqlmodel import Session from .database import get_session from . import crud +from .cards import TECHNIQUE_SUGGESTIONS router = APIRouter() +# Secret Pirate Technique name pool (for the frontend's Suggest buttons) +@router.get("/suggestions/techniques") +def technique_suggestions(): + return {"techniques": TECHNIQUE_SUGGESTIONS} + # Save basic character details @router.post("/game/{game_id}/player/{player_id}/save-basic") def player_save_basic_details( diff --git a/src/pirats/routes_lobby.py b/src/pirats/routes_lobby.py index 13cf3c4..d750940 100644 --- a/src/pirats/routes_lobby.py +++ b/src/pirats/routes_lobby.py @@ -1,4 +1,3 @@ -from typing import Optional from fastapi import APIRouter, Depends, HTTPException from sqlmodel import Session from .database import get_session diff --git a/src/pirats/routes_scene.py b/src/pirats/routes_scene.py index 896ea1e..985622c 100644 --- a/src/pirats/routes_scene.py +++ b/src/pirats/routes_scene.py @@ -1,4 +1,3 @@ -from typing import Optional from fastapi import APIRouter, Form, Depends, HTTPException from fastapi.responses import JSONResponse from sqlmodel import Session diff --git a/src/pirats/routes_upkeep.py b/src/pirats/routes_upkeep.py index b59ce13..8780bd1 100644 --- a/src/pirats/routes_upkeep.py +++ b/src/pirats/routes_upkeep.py @@ -1,5 +1,5 @@ import json -from fastapi import APIRouter, Form, Depends, HTTPException, status +from fastapi import APIRouter, Form, Depends, HTTPException from fastapi.responses import JSONResponse from sqlmodel import Session from .database import get_session diff --git a/src/pirats/static/css/admin.css b/src/pirats/static/css/admin.css deleted file mode 100644 index b5a9568..0000000 --- a/src/pirats/static/css/admin.css +++ /dev/null @@ -1,28 +0,0 @@ -/* --- Admin Panel --- */ -.admin-players-list { - display: flex; - flex-direction: column; - gap: 1rem; - margin-top: 1rem; -} - -.admin-player-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem; - gap: 1rem; - flex-wrap: wrap; -} - -.player-info-basic { - font-size: 1.05rem; -} - -.link-copy-action { - display: flex; - gap: 0.5rem; - align-items: center; - flex: 1; - max-width: 500px; -} diff --git a/src/pirats/static/css/card.css b/src/pirats/static/css/card.css deleted file mode 100644 index 4eb3161..0000000 --- a/src/pirats/static/css/card.css +++ /dev/null @@ -1,244 +0,0 @@ -/* Card Widget (Mini) */ -.card-mini { - width: 32px; - height: 48px; - border-radius: 4px; - border: 1px solid var(--text-muted); - background: var(--bg-dark); - display: flex; - flex-direction: column; - justify-content: space-between; - padding: 2px 4px; - font-size: 0.7rem; - font-weight: 900; -} - -.card-mini.base-card { - border-color: var(--gold); - color: var(--gold); - background: rgba(212,175,55,0.1); -} - -.card-mini.rotated { - transform: rotate(90deg); - margin: 0 4px; -} - -.card-mini.success { - border-color: var(--neon-emerald); - color: var(--neon-emerald); -} - -.card-mini.failure { - border-color: var(--red-suit); - color: var(--red-suit); -} - -.card-mini .val { - align-self: flex-start; -} - -.card-mini .suit { - align-self: flex-end; -} - -.card-mini .owner { - font-size: 0.5rem; - text-transform: uppercase; - position: absolute; - bottom: -1px; - left: 1px; - color: var(--text-muted); -} - -/* Card Widget (Medium) */ -.card-medium { - width: 75px; - height: 112px; - border-radius: 6px; - border: 1.5px solid var(--glass-border); - background: linear-gradient(135deg, var(--bg-ocean-light) 0%, var(--bg-dark) 100%); - box-shadow: 0 3px 10px rgba(0,0,0,0.4); - display: flex; - flex-direction: column; - justify-content: space-between; - padding: 0.3rem; - position: relative; - font-size: 0.85rem; -} - -.card-medium .card-corner { - display: flex; - flex-direction: column; - align-items: center; - line-height: 1; -} - -.card-medium .card-corner .val { - font-weight: 900; - font-size: 0.9rem; -} - -.card-medium .card-corner .suit { - font-size: 0.75rem; -} - -.card-medium.suit-c, .card-medium.suit-s { - border-color: rgba(226, 232, 240, 0.15); -} - -.card-medium.suit-h, .card-medium.suit-d { - border-color: rgba(255, 42, 95, 0.15); -} - -.card-medium.joker-card { - border-color: var(--gold); - background: linear-gradient(135deg, #1d1b10 0%, var(--bg-dark) 100%); -} - -.card-medium.suit-c .val, .card-medium.suit-c .suit, -.card-medium.suit-s .val, .card-medium.suit-s .suit { - color: var(--black-suit); -} - -.card-medium.suit-h .val, .card-medium.suit-h .suit, -.card-medium.suit-d .val, .card-medium.suit-d .suit { - color: var(--red-suit); -} - -.card-medium .card-center { - font-size: 1.8rem; - text-align: center; - align-self: center; - margin: auto 0; -} - -.card-medium.suit-c .card-center, .card-medium.suit-s .card-center { color: var(--black-suit); } -.card-medium.suit-h .card-center, .card-medium.suit-d .card-center { color: var(--red-suit); } - -/* Card Widget (Large) */ -.card-large { - width: 110px; - height: 165px; - border-radius: 8px; - border: 2px solid var(--glass-border); - background: linear-gradient(135deg, var(--bg-ocean-light) 0%, var(--bg-dark) 100%); - box-shadow: 0 4px 15px rgba(0,0,0,0.5); - display: flex; - flex-direction: column; - justify-content: space-between; - padding: 0.5rem; - position: relative; - transition: var(--transition-smooth); -} - -.card-large:hover { - transform: translateY(-8px) scale(1.05); - border-color: var(--gold); - box-shadow: 0 10px 25px rgba(212, 175, 55, 0.2); -} - -.card-large.suit-c, .card-large.suit-s { - border-color: rgba(226, 232, 240, 0.15); -} -.card-large.suit-c:hover, .card-large.suit-s:hover { - border-color: var(--black-suit); - box-shadow: 0 10px 25px var(--black-glow); -} - -.card-large.suit-h, .card-large.suit-d { - border-color: rgba(255, 42, 95, 0.15); -} -.card-large.suit-h:hover, .card-large.suit-d:hover { - border-color: var(--red-suit); - box-shadow: 0 10px 25px var(--red-glow); -} - -.card-large.joker-card { - border-color: var(--gold); - background: linear-gradient(135deg, #1d1b10 0%, var(--bg-dark) 100%); -} - -.card-large .card-corner { - display: flex; - flex-direction: column; - align-items: center; - line-height: 1.1; -} - -.card-large .card-corner .val { - font-size: 1.2rem; - font-weight: 900; -} - -.card-large .card-corner .suit { - font-size: 1rem; -} - -.card-large.suit-c .val, .card-large.suit-c .suit, -.card-large.suit-s .val, .card-large.suit-s .suit { - color: var(--black-suit); -} - -.card-large.suit-h .val, .card-large.suit-h .suit, -.card-large.suit-d .val, .card-large.suit-d .suit { - color: var(--red-suit); -} - -.card-large .card-center { - font-size: 2.8rem; - text-align: center; - align-self: center; - margin: auto 0; -} - -.card-large.suit-c .card-center, .card-large.suit-s .card-center { color: var(--black-suit); } -.card-large.suit-h .card-center, .card-large.suit-d .card-center { color: var(--red-suit); } - -.card-large .card-tech-overlay { - position: absolute; - bottom: 0.5rem; - left: 0.5rem; - right: 0.5rem; - text-align: center; -} - -.tech-tag { - background: rgba(0, 0, 0, 0.7); - border: 1px solid var(--gold); - color: var(--gold); - font-size: 0.65rem; - padding: 0.15rem 0.3rem; - border-radius: 4px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.joker-action { - display: flex; - gap: 2px; -} - -/* Drag and Drop Interface Styles */ -.card-large[draggable="true"] { - cursor: grab; -} - -.card-large[draggable="true"]:active { - cursor: grabbing; -} - -.card-large.dragging { - opacity: 0.4; - border-style: dashed; - transform: scale(0.95); -} - -.obstacle-item.drag-over { - border-color: var(--gold) !important; - background: rgba(212, 175, 55, 0.1) !important; - box-shadow: 0 0 20px rgba(212, 175, 55, 0.2) !important; - transform: translateY(-2px) scale(1.01); - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); -} diff --git a/src/pirats/static/css/character.css b/src/pirats/static/css/character.css deleted file mode 100644 index 13ea2f4..0000000 --- a/src/pirats/static/css/character.css +++ /dev/null @@ -1,442 +0,0 @@ -/* --- Character Creation & Sheet Grid --- */ -.creation-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - margin-top: 2rem; -} - -@media (max-width: 900px) { - .creation-grid { - grid-template-columns: 1fr; - } -} - -.section-desc { - font-size: 0.9rem; - color: var(--text-muted); - margin-bottom: 1rem; -} - -.record-line { - background: rgba(0,0,0,0.2); - padding: 0.75rem 1rem; - border-radius: 6px; - margin-bottom: 0.75rem; - border-left: 3px solid var(--gold); -} - -.delegation-box { - margin-bottom: 1.25rem; - padding: 1.25rem; - text-align: left; -} - -.delegation-box h4 { - font-size: 0.95rem; - color: var(--text-primary); - margin-bottom: 0.5rem; -} - -.answer-box { - font-style: italic; - color: var(--neon-cyan); - font-size: 1.05rem; -} - -.author-label { - font-size: 0.8rem; - color: var(--text-muted); - font-style: normal; -} - -.waiting-box { - font-size: 0.9rem; - color: var(--gold); - display: flex; - align-items: center; - gap: 0.5rem; -} - -.inbox-list:has(.inbox-item) .inbox-empty-message { - display: none; -} - -.inbox-item { - padding: 1rem; - margin-bottom: 0.75rem; - border-left: 3px solid var(--neon-cyan); - background: rgba(0, 242, 254, 0.02); -} - -.inbox-item label { - display: block; - margin-bottom: 0.5rem; - font-size: 0.95rem; -} - -.submitted-techniques .tech-chip { - background: rgba(212,175,55,0.05); - border: 1px solid var(--gold); - color: var(--gold); - padding: 0.5rem; - border-radius: 6px; - margin-bottom: 0.5rem; - font-family: var(--font-heading); -} - -.tech-chip { - padding: 0.6rem 1rem; - font-size: 0.95rem; -} - -.tech-assignment-pill { - background: rgba(0, 242, 254, 0.05); - border: 1px solid var(--neon-cyan); - color: var(--text-primary); - padding: 0.6rem 1rem; - border-radius: 6px; - margin-bottom: 0.5rem; - text-align: left; - display: flex; - align-items: center; - gap: 1rem; -} - -.card-rank { - background: var(--neon-cyan); - color: var(--bg-dark); - font-weight: 900; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - font-size: 0.85rem; -} - -/* --- Character Sheet layout --- */ -.character-sheet-details { - text-align: left; -} - -.sheet-group { - background: rgba(0,0,0,0.15); - padding: 1rem; - border-radius: 8px; - border-left: 2px solid var(--gold-dark); -} - -.sheet-group h4 { - color: var(--gold); - font-size: 0.95rem; - margin-bottom: 0.5rem; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.footnote { - font-size: 0.8rem; - color: var(--text-muted); -} - -.techniques-list-sheet { - list-style: none; -} - -.techniques-list-sheet li { - margin-bottom: 0.25rem; - font-size: 0.95rem; -} - -.footnote-desc { - font-size: 0.8rem; - color: var(--text-muted); - margin-top: 0.15rem; - margin-left: 1.5rem; -} - -.margin-top-small { - margin-top: 0.5rem; -} - -/* --- Visual & Tactile Technique Assignment --- */ -.face-tech-drag-form { - margin-top: 1.5rem; -} - -.available-techniques-container { - background: rgba(255, 255, 255, 0.02); - border: 1px dashed var(--glass-border); - border-radius: 12px; - padding: 1.25rem; - margin-bottom: 2rem; - box-shadow: inset 0 0 15px rgba(0,0,0,0.2); -} - -.available-techniques-container h4 { - font-family: var(--font-heading); - color: var(--gold); - font-size: 1.1rem; - margin-bottom: 0.25rem; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.instruction-help { - font-size: 0.8rem; - color: var(--text-muted); - margin-bottom: 1rem; -} - -.techniques-drag-pool { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - justify-content: center; - min-height: 50px; - align-items: center; -} - -.draggable-tech-chip { - background: linear-gradient(135deg, rgba(22, 36, 59, 0.9) 0%, rgba(13, 23, 38, 0.9) 100%); - border: 1px solid var(--gold); - color: var(--text-primary); - padding: 0.6rem 1rem; - border-radius: 8px; - cursor: grab; - user-select: none; - font-family: var(--font-body); - font-size: 0.95rem; - font-weight: 500; - transition: var(--transition-smooth); - box-shadow: 0 4px 10px rgba(0,0,0,0.3), 0 0 5px rgba(212, 175, 55, 0.1); - display: flex; - align-items: center; - gap: 0.5rem; -} - -.draggable-tech-chip:hover { - transform: translateY(-2px); - border-color: var(--neon-cyan); - box-shadow: 0 6px 15px rgba(0, 0, 0, 0.4), 0 0 10px rgba(0, 242, 254, 0.3); -} - -.draggable-tech-chip:active { - cursor: grabbing; -} - -.draggable-tech-chip.dragging { - opacity: 0.4; - border-style: dashed; -} - -.draggable-tech-chip.selected { - border-color: var(--neon-cyan); - box-shadow: 0 0 15px rgba(0, 242, 254, 0.5); - background: rgba(0, 242, 254, 0.1); -} - -.draggable-tech-chip.assigned-hidden { - opacity: 0.2; - pointer-events: none; - cursor: not-allowed; - border-color: var(--text-muted); -} - -.drag-icon { - color: var(--gold); - font-size: 0.8rem; - opacity: 0.7; -} - -/* Slots Grid */ -.face-cards-slots-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 1.5rem; - margin-bottom: 2rem; -} - -@media (max-width: 768px) { - .face-cards-slots-grid { - grid-template-columns: 1fr; - } -} - -.face-card-slot-wrapper { - display: flex; - justify-content: center; -} - -.face-card-slot { - position: relative; - width: 100%; - max-width: 220px; - aspect-ratio: 2 / 3; - min-height: 280px; - background: linear-gradient(135deg, rgba(13, 23, 38, 0.8) 0%, rgba(7, 11, 18, 0.9) 100%); - border: 2px dashed rgba(212, 175, 55, 0.3); - border-radius: 12px; - padding: 1rem; - display: flex; - flex-direction: column; - justify-content: space-between; - transition: var(--transition-smooth); - overflow: hidden; - cursor: pointer; - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4); -} - -.face-card-slot:hover { - border-color: rgba(212, 175, 55, 0.7); - box-shadow: 0 12px 30px rgba(212, 175, 55, 0.15); - transform: translateY(-4px); -} - -.face-card-slot.drag-over { - border-color: var(--neon-cyan); - border-style: solid; - background: rgba(0, 242, 254, 0.05); - box-shadow: 0 0 20px rgba(0, 242, 254, 0.2); - transform: scale(1.02); -} - -.face-card-slot.has-assignment { - border-style: solid; - border-color: var(--gold); - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5), 0 0 15px rgba(212, 175, 55, 0.15); -} - -.card-bg-letter { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-family: var(--font-heading); - font-size: 8rem; - font-weight: 900; - color: rgba(255, 255, 255, 0.025); - line-height: 1; - pointer-events: none; - user-select: none; - transition: var(--transition-smooth); -} - -.face-card-slot.has-assignment .card-bg-letter { - color: rgba(212, 175, 55, 0.04); -} - -.card-slot-header, .card-slot-footer { - display: flex; - justify-content: space-between; - align-items: center; - font-family: var(--font-heading); - font-weight: 700; - font-size: 1.1rem; - color: var(--text-muted); - pointer-events: none; - user-select: none; -} - -.face-card-slot.has-assignment .card-slot-header, -.face-card-slot.has-assignment .card-slot-footer { - color: var(--gold); -} - -.card-slot-body { - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; - z-index: 2; - padding: 0.5rem 0; -} - -.card-title { - font-family: var(--font-heading); - font-size: 1.2rem; - font-weight: 700; - color: var(--text-primary); - margin-bottom: 0.75rem; - pointer-events: none; -} - -.face-card-slot.has-assignment .card-title { - color: var(--gold); -} - -.drop-zone-placeholder { - font-size: 0.8rem; - color: var(--text-muted); - border: 1px dashed rgba(255, 255, 255, 0.1); - background: rgba(0, 0, 0, 0.2); - border-radius: 6px; - padding: 0.5rem 0.75rem; - transition: var(--transition-smooth); - pointer-events: none; -} - -.face-card-slot:hover .drop-zone-placeholder { - color: var(--text-primary); - border-color: rgba(255, 255, 255, 0.2); -} - -.assigned-tech-content { - width: 100%; - display: none; -} - -.face-card-slot.has-assignment .drop-zone-placeholder { - display: none; -} - -.face-card-slot.has-assignment .assigned-tech-content { - display: block; -} - -.assigned-tech-chip { - background: rgba(212, 175, 55, 0.08); - border: 1px solid var(--gold); - color: var(--text-primary); - padding: 0.5rem; - border-radius: 8px; - font-size: 0.85rem; - position: relative; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); - word-break: break-word; - animation: fadeIn 0.3s ease; -} - -.remove-tech-btn { - position: absolute; - top: -8px; - right: -8px; - width: 18px; - height: 18px; - border-radius: 50%; - background: #c0392b; - border: 1px solid #e74c3c; - color: white; - font-size: 10px; - font-weight: 900; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - box-shadow: 0 2px 5px rgba(0,0,0,0.5); - transition: var(--transition-smooth); -} - -.remove-tech-btn:hover { - background: #e74c3c; - transform: scale(1.1); -} - -@keyframes fadeIn { - from { opacity: 0; transform: scale(0.9); } - to { opacity: 1; transform: scale(1); } -} diff --git a/src/pirats/static/css/components.css b/src/pirats/static/css/components.css deleted file mode 100644 index e3abba0..0000000 --- a/src/pirats/static/css/components.css +++ /dev/null @@ -1,301 +0,0 @@ -/* --- UI Panels & Glassmorphism --- */ -.glass-panel { - background: var(--glass-bg); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid var(--glass-border); - border-radius: 12px; - padding: 2rem; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); - transition: var(--transition-smooth); -} - -.glass-panel:hover { - border-color: rgba(212, 175, 55, 0.35); - box-shadow: 0 8px 32px rgba(212, 175, 55, 0.05); -} - -.card { - background: var(--bg-ocean-light); - border: 1px solid var(--glass-border); - border-radius: 12px; - padding: 1.5rem; - margin-bottom: 1.5rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.3); - transition: var(--transition-smooth); -} - -.card h3 { - font-family: var(--font-heading); - color: var(--gold); - margin-bottom: 1rem; - border-bottom: 1px solid rgba(212, 175, 55, 0.2); - padding-bottom: 0.5rem; -} - -/* --- Forms & Controls --- */ -.form-group { - margin-bottom: 1.25rem; - text-align: left; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-size: 0.95rem; - color: var(--text-muted); - font-weight: 500; -} - -.input-field { - width: 100%; - padding: 0.75rem 1rem; - background: rgba(7, 11, 18, 0.8); - border: 1px solid var(--glass-border); - border-radius: 8px; - color: var(--text-primary); - font-family: var(--font-body); - font-size: 1rem; - transition: var(--transition-smooth); -} - -.input-field:focus { - outline: none; - border-color: var(--neon-cyan); - box-shadow: 0 0 10px rgba(0, 242, 254, 0.2); -} - -.select-field { - width: 100%; - padding: 0.75rem 1rem; - background: rgba(7, 11, 18, 0.8); - border: 1px solid var(--glass-border); - border-radius: 8px; - color: var(--text-primary); - font-family: var(--font-body); - font-size: 1rem; - transition: var(--transition-smooth); - cursor: pointer; -} - -.select-field:focus { - outline: none; - border-color: var(--neon-cyan); -} - -.input-row { - display: flex; - gap: 0.5rem; -} - -.input-row .input-field { - flex: 1; -} - -/* --- Buttons --- */ -.btn { - display: inline-block; - padding: 0.75rem 1.5rem; - font-family: var(--font-body); - font-weight: 700; - font-size: 1rem; - text-transform: uppercase; - letter-spacing: 1px; - border: none; - border-radius: 8px; - cursor: pointer; - transition: var(--transition-smooth); - text-decoration: none; -} - -.btn-primary { - background: linear-gradient(135deg, var(--gold) 0%, var(--gold-dark) 100%); - color: var(--text-dark); - box-shadow: 0 4px 15px var(--gold-glow); -} - -.btn-primary:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(212, 175, 55, 0.6); -} - -.btn-secondary { - background: rgba(255,255,255,0.08); - border: 1px solid rgba(255,255,255,0.15); - color: var(--text-primary); -} - -.btn-secondary:hover:not(:disabled) { - background: rgba(255,255,255,0.15); - transform: translateY(-2px); -} - -.btn-deep { - background: linear-gradient(135deg, #152238 0%, #0d1726 100%); - border: 1px solid var(--neon-cyan); - color: var(--neon-cyan); - box-shadow: 0 4px 15px rgba(0, 242, 254, 0.1); -} - -.btn-deep:hover:not(:disabled) { - background: var(--neon-cyan); - color: var(--text-dark); - box-shadow: 0 4px 20px rgba(0, 242, 254, 0.4); - transform: translateY(-2px); -} - -.btn-danger { - background: linear-gradient(135deg, #7a1c1c 0%, #c0392b 100%); - color: var(--text-primary); - box-shadow: 0 4px 15px rgba(192, 57, 43, 0.2); -} - -.btn-danger:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(192, 57, 43, 0.5); -} - -.btn-gold { - background: transparent; - border: 2px solid var(--gold); - color: var(--gold); - box-shadow: 0 0 10px rgba(212,175,55,0.1); -} - -.btn-gold:hover:not(:disabled) { - background: var(--gold); - color: var(--bg-dark); - box-shadow: 0 0 20px var(--gold-glow); - transform: translateY(-2px); -} - -.btn-full { - width: 100%; -} - -.btn-small { - padding: 0.4rem 0.8rem; - font-size: 0.8rem; -} - -.btn-large { - padding: 1rem 2rem; - font-size: 1.1rem; -} - -.btn:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none !important; - box-shadow: none !important; -} - -.glow-effect:hover { - filter: brightness(1.2); -} - -/* --- Event Log --- */ -.event-log-fab { - position: fixed; - bottom: 20px; - right: 20px; - width: 60px; - height: 60px; - border-radius: 50%; - background: var(--glass-bg); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - border: 1px solid var(--gold); - color: var(--gold); - font-size: 1.5rem; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5), 0 0 10px rgba(212, 175, 55, 0.2); - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - z-index: 1000; - transition: var(--transition-smooth); -} - -.event-log-fab:hover { - transform: scale(1.1); - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.6), 0 0 15px rgba(212, 175, 55, 0.4); -} - -.event-log-panel { - position: fixed; - bottom: 90px; - right: 20px; - width: 350px; - max-width: calc(100vw - 40px); - height: 400px; - max-height: calc(100vh - 120px); - background: var(--glass-bg); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid var(--glass-border); - border-radius: 12px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); - display: flex; - flex-direction: column; - z-index: 999; - transition: transform 0.3s ease, opacity 0.3s ease; -} - -.event-log-panel.hidden { - transform: translateY(20px) scale(0.95); - opacity: 0; - pointer-events: none; -} - -.event-log-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); -} - -.event-log-header h3 { - margin: 0; - font-family: var(--font-heading); - font-size: 1.1rem; - color: var(--gold); -} - -.event-log-container { - flex: 1; - overflow-y: auto; - padding: 1rem; - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.event-list { - list-style: none; - padding: 0; - margin: 0; - display: flex; - flex-direction: column; - gap: 0.8rem; -} - -.event-item { - font-size: 0.9rem; - background: rgba(0, 0, 0, 0.3); - padding: 0.75rem; - border-radius: 8px; - border-left: 3px solid var(--neon-cyan); -} - -.event-time { - display: block; - font-size: 0.75rem; - color: var(--text-muted); - margin-bottom: 0.25rem; -} - -.event-msg { - color: var(--text-primary); -} diff --git a/src/pirats/static/css/core.css b/src/pirats/static/css/core.css deleted file mode 100644 index 52c46e8..0000000 --- a/src/pirats/static/css/core.css +++ /dev/null @@ -1,231 +0,0 @@ -/* --- Variables & Tokens --- */ -:root { - --bg-dark: #070b12; - --bg-ocean: #0d1726; - --bg-ocean-light: #16243b; - - --gold: #d4af37; - --gold-glow: rgba(212, 175, 55, 0.4); - --gold-dark: #aa841c; - - --neon-cyan: #00f2fe; - --neon-emerald: #00ff87; - - --red-suit: #ff2a5f; - --red-glow: rgba(255, 42, 95, 0.3); - --black-suit: #e2e8f0; - --black-glow: rgba(226, 232, 240, 0.2); - - --glass-bg: rgba(13, 23, 38, 0.7); - --glass-bg-hover: rgba(22, 36, 59, 0.85); - --glass-border: rgba(212, 175, 55, 0.2); - --glass-border-focus: rgba(0, 242, 254, 0.4); - - --text-primary: #f1f5f9; - --text-muted: #94a3b8; - --text-dark: #0f172a; - - --font-heading: 'Cinzel', serif; - --font-body: 'Outfit', sans-serif; - - --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -/* --- Base Reset & Setup --- */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - background: radial-gradient(circle at 50% 50%, var(--bg-ocean) 0%, var(--bg-dark) 100%); - color: var(--text-primary); - font-family: var(--font-body); - font-size: 16px; - line-height: 1.6; - min-height: 100vh; - display: flex; - flex-direction: column; - overflow-x: hidden; -} - -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 8px; -} -::-webkit-scrollbar-track { - background: var(--bg-dark); -} -::-webkit-scrollbar-thumb { - background: var(--gold-dark); - border-radius: 4px; -} -::-webkit-scrollbar-thumb:hover { - background: var(--gold); -} - -/* --- Layout Components --- */ -.app-header { - background: rgba(7, 11, 18, 0.95); - border-bottom: 2px solid var(--gold); - box-shadow: 0 4px 20px rgba(0,0,0,0.5); - padding: 1rem 2rem; - display: flex; - justify-content: space-between; - align-items: center; - position: sticky; - top: 0; - z-index: 100; -} - -.logo-container { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.logo-container h1 { - font-family: var(--font-heading); - font-size: 1.8rem; - font-weight: 900; - letter-spacing: 2px; - background: linear-gradient(135deg, var(--gold) 30%, #fff 70%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - text-shadow: 0 0 10px var(--gold-glow); -} - -.math-magic { - font-family: var(--font-heading); - font-size: 2.2rem; - color: var(--neon-cyan); - text-shadow: 0 0 10px var(--neon-cyan), 0 0 20px var(--neon-cyan); - animation: pulse-glow 3s infinite ease-in-out; -} - -.header-status { - display: flex; - align-items: center; - gap: 1rem; -} - -.player-pill { - background: rgba(0, 242, 254, 0.1); - border: 1px solid var(--neon-cyan); - padding: 0.25rem 0.75rem; - border-radius: 20px; - font-size: 0.9rem; - font-weight: 700; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.player-pill .bullet { - width: 8px; - height: 8px; - background-color: var(--neon-emerald); - border-radius: 50%; - box-shadow: 0 0 8px var(--neon-emerald); -} - -.phase-pill { - background: rgba(212, 175, 55, 0.1); - border: 1px solid var(--gold); - color: var(--gold); - padding: 0.25rem 0.75rem; - border-radius: 20px; - font-size: 0.9rem; - font-weight: 700; - letter-spacing: 1px; -} - -.app-main { - flex: 1; - padding: 2rem; - max-width: 1400px; - width: 100%; - margin: 0 auto; -} - -.app-footer { - background: var(--bg-dark); - padding: 1.5rem; - text-align: center; - border-top: 1px solid rgba(255,255,255,0.05); - font-size: 0.85rem; - color: var(--text-muted); -} - -/* --- Loader / Spinner widgets --- */ -.loader-container { - padding: 4rem; -} - -.spinner { - width: 50px; - height: 50px; - border: 3px solid rgba(0, 242, 254, 0.1); - border-radius: 50%; - border-top-color: var(--neon-cyan); - animation: spin 1s ease-in-out infinite; - margin: 0 auto 1.5rem; -} - -.spinner-small { - width: 20px; - height: 20px; - border: 2px solid rgba(255,255,255,0.1); - border-radius: 50%; - border-top-color: var(--gold); - animation: spin 1s ease-in-out infinite; - display: inline-block; -} - -/* --- Alert styles --- */ -.alert { - padding: 0.75rem 1.25rem; - margin-bottom: 1.5rem; - border-radius: 8px; - text-align: center; - font-weight: 500; -} - -.alert-danger { - background: rgba(192, 57, 43, 0.2); - border: 1px solid #c0392b; - color: #e74c3c; -} - -/* --- Animation keyframes --- */ -@keyframes spin { - to { transform: rotate(360deg); } -} - -@keyframes pulse-glow { - 0% { - text-shadow: 0 0 10px var(--neon-cyan), 0 0 20px var(--neon-cyan); - transform: scale(1); - } - 50% { - text-shadow: 0 0 15px var(--neon-cyan), 0 0 30px var(--neon-cyan), 0 0 40px var(--neon-cyan); - transform: scale(1.05); - } - 100% { - text-shadow: 0 0 10px var(--neon-cyan), 0 0 20px var(--neon-cyan); - transform: scale(1); - } -} - -/* --- Utilities --- */ -.text-center { text-align: center; } -.text-left { text-align: left; } -.margin-top { margin-top: 1.5rem; } -.gold-text { color: var(--gold); } -.center-block { margin: 1rem auto; max-width: 500px; } -.inline-form { display: flex; gap: 0.5rem; width: 100%; } -.inline-group { display: flex; gap: 0.5rem; width: 100%; } -.select-small { padding: 0.5rem; font-size: 0.9rem; flex: 1; } -.select-xsmall { padding: 0.4rem; font-size: 0.85rem; } -.btn-small { padding: 0.4rem 0.8rem; font-size: 0.85rem; } diff --git a/src/pirats/static/css/lobby.css b/src/pirats/static/css/lobby.css deleted file mode 100644 index 9b48fe1..0000000 --- a/src/pirats/static/css/lobby.css +++ /dev/null @@ -1,106 +0,0 @@ -/* --- Lobby / Hold View --- */ -.lobby-view { - max-width: 700px; - margin: 2rem auto; -} - -.lobby-view h2 { - font-family: var(--font-heading); - font-size: 2.2rem; - color: var(--gold); - margin-bottom: 0.5rem; -} - -.lobby-status-box { - margin: 2rem 0; -} - -.player-chips { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - justify-content: center; - margin-top: 1rem; -} - -.player-chip { - background: rgba(255,255,255,0.06); - border: 1px solid rgba(255,255,255,0.1); - padding: 0.5rem 1.25rem; - border-radius: 30px; - display: flex; - align-items: center; - gap: 0.5rem; - transition: var(--transition-smooth); -} - -.player-chip.current-user { - background: rgba(0, 242, 254, 0.15); - border-color: var(--neon-cyan); - box-shadow: 0 0 10px rgba(0,242,254,0.15); -} - -.player-chip.voted-chip { - border-color: var(--neon-emerald); - background-color: rgba(0,255,135,0.05); -} - -.player-chip.not-voted-chip { - border-color: rgba(255,255,255,0.1); -} - -.creator-badge { - background: var(--gold); - color: var(--text-dark); - font-size: 0.7rem; - font-weight: 700; - padding: 0.1rem 0.4rem; - border-radius: 4px; - text-transform: uppercase; -} - -.links-box { - text-align: left; - margin-bottom: 2rem; -} - -.link-item { - margin-bottom: 1.5rem; -} - -.link-item h4 { - color: var(--text-primary); - margin-bottom: 0.5rem; -} - -.copy-container { - display: flex; - gap: 0.5rem; -} - -.copy-input { - flex: 1; - background: #05080e; - border: 1px solid var(--glass-border); - color: var(--text-primary); - padding: 0.5rem 1rem; - border-radius: 8px; - font-family: var(--font-body); - font-size: 0.95rem; -} - -.copy-input.admin-input { - border-color: var(--gold-dark); - color: var(--gold); -} - -.admin-link-item { - border-top: 1px dashed rgba(212, 175, 55, 0.3); - padding-top: 1.5rem; -} - -.info-text { - font-size: 0.9rem; - color: var(--text-muted); - margin-bottom: 0.5rem; -} diff --git a/src/pirats/static/css/scene-play.css b/src/pirats/static/css/scene-play.css deleted file mode 100644 index c0a969f..0000000 --- a/src/pirats/static/css/scene-play.css +++ /dev/null @@ -1,277 +0,0 @@ -/* --- Scene Play Board Layout --- */ -.scene-view-layout { - display: grid; - grid-template-columns: 1.4fr 1fr; - gap: 2rem; -} - -@media (max-width: 1100px) { - .scene-view-layout { - grid-template-columns: 1fr; - } -} - -.card-header { - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid rgba(212, 175, 55, 0.2); - padding-bottom: 0.5rem; - margin-bottom: 1rem; -} - -.card-header h3 { - border-bottom: none; - margin-bottom: 0; - padding-bottom: 0; -} - -.deck-counter { - font-size: 0.85rem; - color: var(--gold); - font-weight: 700; -} - -/* --- Active Obstacle Item Display --- */ -.obstacles-container { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.obstacle-item { - border: 1px solid var(--glass-border); - border-radius: 10px; - padding: 1.25rem; - background: rgba(7, 11, 18, 0.4); - display: grid; - grid-template-columns: 0.8fr 2fr 1fr 1fr; - gap: 1rem; - position: relative; - overflow: hidden; -} - -@media (max-width: 768px) { - .obstacle-item { - grid-template-columns: 0.8fr 2fr 1.5fr; - } -} - -@media (max-width: 600px) { - .obstacle-item { - grid-template-columns: 1fr; - } -} - -/* Color theme mappings for suits */ -.suit-c { border-left: 4px solid var(--black-suit); } -.suit-s { border-left: 4px solid var(--black-suit); } -.suit-h { border-left: 4px solid var(--red-suit); } -.suit-d { border-left: 4px solid var(--red-suit); } - -.obstacle-meta { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - background: rgba(0,0,0,0.3); - border-radius: 6px; - padding: 0.5rem; -} - -.suit-badge { - font-weight: 900; - font-size: 1.1rem; -} - -.suit-c .suit-badge, .suit-s .suit-badge { color: var(--black-suit); } -.suit-h .suit-badge, .suit-d .suit-badge { color: var(--red-suit); } - -.card-code { - font-size: 0.8rem; - color: var(--text-muted); - font-family: var(--font-heading); -} - -.obstacle-details h4 { - font-family: var(--font-heading); - color: var(--text-primary); - margin-bottom: 0.25rem; - font-size: 1.1rem; -} - -.obstacle-details .desc { - font-size: 0.85rem; - color: var(--text-muted); -} - -.obstacle-value-display { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - background: rgba(212, 175, 55, 0.04); - border: 1px dashed var(--glass-border); - border-radius: 6px; - padding: 0.5rem; -} - -.obstacle-successes-display { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - background: rgba(0, 255, 135, 0.04); - border: 1px dashed rgba(0, 255, 135, 0.25); - border-radius: 6px; - padding: 0.5rem; -} - -.obstacle-successes-display .val-number { - color: var(--neon-emerald); -} - -.val-label { - font-size: 0.75rem; - color: var(--text-muted); - text-transform: uppercase; -} - -.val-number { - font-size: 1.8rem; - font-family: var(--font-heading); - font-weight: 900; - color: var(--gold); -} - -.played-column { - grid-column: span 4; - border-top: 1px solid rgba(255,255,255,0.05); - padding-top: 0.75rem; - margin-top: 0.5rem; -} - -@media (max-width: 600px) { - .played-column { - grid-column: span 1; - } -} - -.played-column h5 { - font-size: 0.8rem; - color: var(--text-muted); - margin-bottom: 0.5rem; -} - -.column-cards-flex { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 0.75rem; -} - -/* --- Challenges Section --- */ -.challenges-container { - display: flex; - flex-direction: column; - gap: 1.25rem; -} - -.challenge-item { - background: rgba(255,255,255,0.02); - border: 1px solid var(--glass-border); - border-radius: 8px; - padding: 1.25rem; - text-align: left; -} - -.challenge-item h4 { - font-family: var(--font-heading); - color: var(--neon-cyan); - font-size: 1.2rem; - margin-bottom: 0.25rem; -} - -.challenge-item .desc { - font-size: 0.9rem; - color: var(--text-muted); - margin-bottom: 1rem; -} - -.applied-obstacles h5 { - font-size: 0.85rem; - color: var(--text-muted); - margin-bottom: 0.5rem; -} - -.applied-obs-row { - padding: 0.75rem 1rem; - margin-bottom: 0.5rem; - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 0.75rem; -} - -.obs-info { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.obs-info .symbol { - font-size: 1.2rem; -} - -.play-card-form { - display: flex; - gap: 0.5rem; - align-items: center; -} - -.select-xsmall { - padding: 0.25rem 0.5rem; - font-size: 0.8rem; - width: 140px; - background: var(--bg-dark); -} - -/* --- Deep Control panel --- */ -.deep-text { - color: var(--neon-cyan) !important; -} - -.deep-divider { - border: 0; - height: 1px; - background: linear-gradient(to right, rgba(0, 242, 254, 0), rgba(0, 242, 254, 0.4), rgba(0, 242, 254, 0)); - margin: 2rem 0; -} - -.obstacles-checkboxes { - display: flex; - flex-direction: column; - gap: 0.5rem; - background: rgba(0,0,0,0.2); - padding: 1rem; - border-radius: 8px; - max-height: 150px; - overflow-y: auto; - border: 1px solid var(--glass-border); -} - -.checkbox-label { - display: flex; - align-items: center; - gap: 0.5rem; - cursor: pointer; - font-size: 0.95rem; -} - -.hand-flex { - display: flex; - flex-wrap: wrap; - gap: 1rem; - align-items: flex-start; -} diff --git a/src/pirats/static/css/scene-setup.css b/src/pirats/static/css/scene-setup.css deleted file mode 100644 index cc8d232..0000000 --- a/src/pirats/static/css/scene-setup.css +++ /dev/null @@ -1,83 +0,0 @@ -/* --- Scene Setup / Role Choose --- */ -.setup-grid { - display: grid; - grid-template-columns: 1.2fr 1fr; - gap: 2rem; - margin: 2rem 0; -} - -@media (max-width: 800px) { - .setup-grid { - grid-template-columns: 1fr; - } -} - -.role-buttons { - display: flex; - flex-direction: column; - gap: 1.5rem; - margin-top: 1.5rem; -} - -.role-btn { - padding: 1.5rem; - font-size: 1.2rem; - background: rgba(255,255,255,0.03); - border: 2px solid rgba(255,255,255,0.1); - color: var(--text-muted); -} - -.role-btn.pirat-btn:hover, .role-btn.pirat-btn.active { - border-color: var(--neon-emerald); - color: var(--text-primary); - background-color: rgba(0,255,135,0.06); - box-shadow: 0 0 15px rgba(0,255,135,0.15); -} - -.role-btn.deep-btn:hover, .role-btn.deep-btn.active { - border-color: var(--neon-cyan); - color: var(--text-primary); - background-color: rgba(0,242,254,0.06); - box-shadow: 0 0 15px rgba(0,242,254,0.15); -} - -.large-badge { - padding: 0.5rem 2rem; - font-size: 1.5rem; - border-radius: 8px; - font-family: var(--font-heading); - margin-top: 0.5rem; -} - -.role-badge.deep { - background: rgba(0, 242, 254, 0.15); - border: 2px solid var(--neon-cyan); - color: var(--neon-cyan); - box-shadow: 0 0 15px rgba(0,242,254,0.1); -} - -.role-badge.pirat { - background: rgba(0, 255, 135, 0.15); - border: 2px solid var(--neon-emerald); - color: var(--neon-emerald); - box-shadow: 0 0 15px rgba(0,255,135,0.1); -} - -.list-chips { - justify-content: flex-start; -} - -.list-chips .player-chip { - padding: 0.4rem 1rem; - font-size: 0.9rem; -} - -.deep-chip { - border-color: var(--neon-cyan); - background: rgba(0, 242, 254, 0.03); -} - -.pirat-chip { - border-color: var(--neon-emerald); - background: rgba(0, 255, 135, 0.03); -} diff --git a/src/pirats/static/css/style.css b/src/pirats/static/css/style.css deleted file mode 100644 index 2a5d954..0000000 --- a/src/pirats/static/css/style.css +++ /dev/null @@ -1,18 +0,0 @@ -/* ========================================================================== - RATS WITH GATS DESIGN SYSTEM & STYLESHEET - A weathered math-magic pirate aesthetic: deep-ocean dark mode, glassmorphism, - neon math runes, gold accents, and animated card widgets. - - This file imports modular stylesheets divided by page phase and components. - ========================================================================== */ - -@import url("core.css"); -@import url("components.css"); -@import url("card.css"); -@import url("welcome.css"); -@import url("lobby.css"); -@import url("character.css"); -@import url("scene-setup.css"); -@import url("scene-play.css"); -@import url("upkeep.css"); -@import url("admin.css"); diff --git a/src/pirats/static/css/upkeep.css b/src/pirats/static/css/upkeep.css deleted file mode 100644 index 5a949a8..0000000 --- a/src/pirats/static/css/upkeep.css +++ /dev/null @@ -1,100 +0,0 @@ -/* --- Between Scenes Upkeep --- */ -.between-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - margin: 2rem 0; -} - -@media (max-width: 800px) { - .between-grid { - grid-template-columns: 1fr; - } -} - -.voting-card, .deep-rest-card { - text-align: left; -} - -.voted-confirmation-box { - padding: 1.5rem; - text-align: center; - border: 1px solid var(--neon-emerald); -} - -.success-text { - color: var(--neon-emerald); - font-weight: 700; -} - -.ranks-ledger { - list-style: none; -} - -.ranks-ledger li { - padding: 0.5rem; - border-bottom: 1px solid rgba(255,255,255,0.05); - display: flex; - justify-content: space-between; -} - -.deep-badge { - background: rgba(0, 242, 254, 0.15); - border: 1px solid var(--neon-cyan); - color: var(--neon-cyan); - padding: 0.1rem 0.4rem; - font-size: 0.75rem; - border-radius: 4px; -} - -.pirat-badge { - background: rgba(0, 255, 135, 0.15); - border: 1px solid var(--neon-emerald); - color: var(--neon-emerald); - padding: 0.1rem 0.4rem; - font-size: 0.75rem; - border-radius: 4px; -} - -/* --- Deep Upkeep Hand Refresh Drag & Drop --- */ -.upkeep-drag-container { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - margin: 2rem 0; -} - -@media (max-width: 768px) { - .upkeep-drag-container { - grid-template-columns: 1fr; - } -} - -.upkeep-box { - min-height: 400px; - display: flex; - flex-direction: column; -} - -.upkeep-box h3 { - margin-bottom: 1rem; - font-family: var(--font-heading); -} - -.upkeep-flex { - flex: 1; - display: flex; - flex-wrap: wrap; - gap: 1rem; - padding: 1.5rem; - background: rgba(0, 0, 0, 0.3); - border: 1px dashed var(--glass-border); - border-radius: 8px; - align-content: flex-start; - min-height: 300px; - transition: var(--transition-smooth); -} - -.upkeep-box[ondragover]:hover .upkeep-flex { - border-color: var(--neon-cyan); -} diff --git a/src/pirats/static/css/welcome.css b/src/pirats/static/css/welcome.css deleted file mode 100644 index a06e78e..0000000 --- a/src/pirats/static/css/welcome.css +++ /dev/null @@ -1,50 +0,0 @@ -/* --- Welcome / Main Menu Screen --- */ -.welcome-container { - max-width: 900px; - margin: 4rem auto; -} - -.welcome-hero h2 { - font-family: var(--font-heading); - font-size: 2.5rem; - color: var(--text-primary); - margin-bottom: 1rem; -} - -.welcome-hero .subtitle { - font-size: 1.2rem; - color: var(--text-muted); - margin-bottom: 3rem; -} - -.welcome-actions { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; -} - -@media (max-width: 768px) { - .welcome-actions { - grid-template-columns: 1fr; - } -} - -.action-card { - padding: 2.5rem 1.5rem; - display: flex; - flex-direction: column; - justify-content: space-between; - height: 300px; -} - -.action-card h3 { - font-family: var(--font-heading); - color: var(--gold); - font-size: 1.5rem; - margin-bottom: 0.75rem; -} - -.action-card p { - color: var(--text-muted); - margin-bottom: 2rem; -} diff --git a/src/pirats/static/js/suggestions.js b/src/pirats/static/js/suggestions.js deleted file mode 100644 index 2d9f51f..0000000 --- a/src/pirats/static/js/suggestions.js +++ /dev/null @@ -1,1423 +0,0 @@ -// Auto-generated math-pirate suggestions pool for character creation -const SUGGESTIONS = { - "look": [ - "A cyber-pirate waistcoat buttoned with old copper coins", - "A mechanical leather vest covered in algebraic graffiti", - "A salty tail glowing with neon math runes", - "A ink-stained paw replaced with a brass protractor-claw", - "A barnacle-covered paw replaced with a brass protractor-claw", - "A cyber-pirate tricorn hat with a built-in compass", - "A salty tricorn hat with a built-in compass", - "A scurvy snout that twitches in rhythm with prime numbers", - "A brass-plated coat patterned like a checkerboard", - "A brass telescope permanently strapped to the left ear", - "A neon-glowing tail glowing with neon math runes", - "A scurvy paw replaced with a brass protractor-claw", - "A ancient whisker arrangement forming a perfect sine wave", - "A brass-plated leather vest covered in algebraic graffiti", - "A brass-plated peg-leg with a hidden slide-rule drawer", - "A polished tail glowing with neon math runes", - "A barnacle-covered snout with geometry scars", - "A ancient bandana soaked in salty seawater and ink", - "A ancient coat patterned like a checkerboard", - "A rusty snout that twitches in rhythm with prime numbers", - "A weather-beaten tail curled into a Fibonacci spiral", - "A gilded paw replaced with a brass protractor-claw", - "A gilded tricorn hat with a built-in compass", - "A brass-plated tail glowing with neon math runes", - "A mechanical satchel full of crumpled grid paper", - "A mechanical tricorn hat with nested abacus beads", - "A one-eyed leather vest covered in algebraic graffiti", - "A ancient tail glowing with neon math runes", - "A rusty leather vest covered in algebraic graffiti", - "A one-eyed paw replaced with a brass protractor-claw", - "A phosphorescent tail curled into a Fibonacci spiral", - "A weather-beaten paw replaced with a brass protractor-claw", - "A brass-plated whisker arrangement forming a perfect sine wave", - "A ancient snout that twitches in rhythm with prime numbers", - "A polished tricorn hat with a built-in compass", - "A scurvy whisker arrangement forming a perfect sine wave", - "A polished paw replaced with a brass protractor-claw", - "A gilded tricorn hat with nested abacus beads", - "A scarf patterned with infinite decimals of Pi", - "A cyber-pirate leather vest covered in algebraic graffiti", - "A barnacle-covered bandana soaked in salty seawater and ink", - "A phosphorescent tricorn hat with a built-in compass", - "A gilded waistcoat buttoned with old copper coins", - "A cyber-pirate satchel full of crumpled grid paper", - "A tricorn hat folded from an old calculus exam", - "A barnacle-covered tail curled into a Fibonacci spiral", - "A barnacle-covered satchel full of crumpled grid paper", - "A neon-glowing eyepatch displaying coordinate maps", - "A weather-beaten bandana soaked in salty seawater and ink", - "A gilded whisker arrangement forming a perfect sine wave", - "A one-eyed coat patterned like a checkerboard", - "A polished eyepatch displaying coordinate maps", - "A salty tricorn hat with nested abacus beads", - "A gilded eyepatch displaying coordinate maps", - "A mechanical waistcoat buttoned with old copper coins", - "A phosphorescent coat patterned like a checkerboard", - "A tiny chalkboard strapped to the forearm for quick calculations", - "A phosphorescent waistcoat buttoned with old copper coins", - "A ancient eyepatch displaying coordinate maps", - "A brass-plated eyepatch displaying coordinate maps", - "Whiskers braided with tiny colorful abacus beads", - "A salty satchel full of crumpled grid paper", - "A barnacle-covered eyepatch displaying coordinate maps", - "A cyber-pirate tail curled into a Fibonacci spiral", - "A cyber-pirate whisker arrangement forming a perfect sine wave", - "A weather-beaten snout with geometry scars", - "A ink-stained waistcoat buttoned with old copper coins", - "A neon-glowing tail curled into a Fibonacci spiral", - "A ink-stained eyepatch displaying coordinate maps", - "A weather-beaten whisker arrangement forming a perfect sine wave", - "A brass-plated tricorn hat with nested abacus beads", - "A polished waistcoat buttoned with old copper coins", - "A salty tail curled into a Fibonacci spiral", - "A scurvy satchel full of crumpled grid paper", - "A gilded tail glowing with neon math runes", - "A scurvy coat patterned like a checkerboard", - "A barnacle-covered tricorn hat with a built-in compass", - "A bandana decorated with coordinate grids", - "A barnacle-covered tricorn hat with nested abacus beads", - "A weather-beaten tail glowing with neon math runes", - "A rusty tail glowing with neon math runes", - "A scurvy waistcoat buttoned with old copper coins", - "A mechanical peg-leg with a hidden slide-rule drawer", - "A silver claw replacement shaped like a protractor", - "A salty paw replaced with a brass protractor-claw", - "A golden tooth engraved with the division sign", - "A ancient tail curled into a Fibonacci spiral", - "A barnacle-covered peg-leg with a hidden slide-rule drawer", - "A barnacle-covered waistcoat buttoned with old copper coins", - "A ink-stained whisker arrangement forming a perfect sine wave", - "A scurvy peg-leg with a hidden slide-rule drawer", - "A ink-stained snout that twitches in rhythm with prime numbers", - "A one-eyed tail glowing with neon math runes", - "A salty peg-leg with a hidden slide-rule drawer", - "A phosphorescent bandana soaked in salty seawater and ink", - "A cyber-pirate snout that twitches in rhythm with prime numbers", - "A ancient tricorn hat with a built-in compass", - "A one-eyed satchel full of crumpled grid paper", - "A rusty paw replaced with a brass protractor-claw", - "A salty waistcoat buttoned with old copper coins", - "A brass-plated tricorn hat with a built-in compass", - "A ancient paw replaced with a brass protractor-claw", - "An eye patch made of a black spade card", - "A ink-stained coat patterned like a checkerboard", - "A barnacle-covered whisker arrangement forming a perfect sine wave", - "A neon-glowing bandana soaked in salty seawater and ink", - "A barnacle-covered tail glowing with neon math runes", - "A one-eyed waistcoat buttoned with old copper coins", - "Fur bleached in the pattern of a Venn diagram", - "A one-eyed snout that twitches in rhythm with prime numbers", - "A one-eyed snout with geometry scars", - "A ink-stained leather vest covered in algebraic graffiti", - "A polished satchel full of crumpled grid paper", - "A brass-plated snout with geometry scars", - "A brass-plated tail curled into a Fibonacci spiral", - "A neon-glowing snout with geometry scars", - "Fur that smells faintly of chalk dust and gunpowder", - "A cyber-pirate paw replaced with a brass protractor-claw", - "A weather-beaten eyepatch displaying coordinate maps", - "A brass-plated satchel full of crumpled grid paper", - "A ancient snout with geometry scars", - "A ancient satchel full of crumpled grid paper", - "A miniature compass hanging from the left nostril ring", - "A polished tricorn hat with nested abacus beads", - "A cyber-pirate tail glowing with neon math runes", - "A one-eyed eyepatch displaying coordinate maps", - "A ink-stained tricorn hat with a built-in compass", - "A cyber-pirate eyepatch displaying coordinate maps", - "A one-eyed peg-leg with a hidden slide-rule drawer", - "A peg leg carved from a heavy wooden slide rule", - "A barnacle-covered coat patterned like a checkerboard", - "A phosphorescent tail glowing with neon math runes", - "A salty snout that twitches in rhythm with prime numbers", - "A cyber-pirate snout with geometry scars", - "A ink-stained snout with geometry scars", - "A glowing neon abacus tattooed on their back", - "A rusty peg-leg with a hidden slide-rule drawer", - "A rusty snout with geometry scars", - "An abacus-wheel peg leg that rolls instead of clicks", - "A neon-glowing waistcoat buttoned with old copper coins", - "A polished coat patterned like a checkerboard", - "A mechanical coat patterned like a checkerboard", - "A phosphorescent tricorn hat with nested abacus beads", - "A brass-plated waistcoat buttoned with old copper coins", - "A barnacle-covered leather vest covered in algebraic graffiti", - "A ancient tricorn hat with nested abacus beads", - "Ear notched in the shape of a perfect right angle", - "A ink-stained tricorn hat with nested abacus beads", - "A mechanical paw replaced with a brass protractor-claw", - "A weather-beaten tricorn hat with a built-in compass", - "A gilded satchel full of crumpled grid paper", - "A ink-stained satchel full of crumpled grid paper", - "A salty coat patterned like a checkerboard", - "A gilded peg-leg with a hidden slide-rule drawer", - "A ink-stained bandana soaked in salty seawater and ink", - "A gilded tail curled into a Fibonacci spiral", - "A mechanical whisker arrangement forming a perfect sine wave", - "A phosphorescent peg-leg with a hidden slide-rule drawer", - "A polished bandana soaked in salty seawater and ink", - "A monocle that displays neon green equations", - "A polished snout that twitches in rhythm with prime numbers", - "A mechanical tail curled into a Fibonacci spiral", - "A phosphorescent leather vest covered in algebraic graffiti", - "A scurvy tail curled into a Fibonacci spiral", - "A scurvy eyepatch displaying coordinate maps", - "A phosphorescent snout that twitches in rhythm with prime numbers", - "A neon-glowing snout that twitches in rhythm with prime numbers", - "A salty whisker arrangement forming a perfect sine wave", - "A phosphorescent paw replaced with a brass protractor-claw", - "A scurvy bandana soaked in salty seawater and ink", - "A neon-glowing whisker arrangement forming a perfect sine wave", - "A scurvy snout with geometry scars", - "A neon-glowing tricorn hat with nested abacus beads", - "A brass-plated bandana soaked in salty seawater and ink", - "A scurvy tricorn hat with nested abacus beads", - "A ancient peg-leg with a hidden slide-rule drawer", - "A mechanical bandana soaked in salty seawater and ink", - "A polished leather vest covered in algebraic graffiti", - "A gilded snout that twitches in rhythm with prime numbers", - "A scurvy tail glowing with neon math runes", - "A neon-glowing leather vest covered in algebraic graffiti", - "A brass-plated snout that twitches in rhythm with prime numbers", - "A rusty coat patterned like a checkerboard", - "A ink-stained tail glowing with neon math runes", - "A tail wrapped in copper wire that sparks when they get excited", - "A gilded leather vest covered in algebraic graffiti", - "A salty eyepatch displaying coordinate maps", - "A weather-beaten waistcoat buttoned with old copper coins", - "A gilded snout with geometry scars", - "A mechanical eyepatch displaying coordinate maps", - "A rusty waistcoat buttoned with old copper coins", - "A cyber-pirate tricorn hat with nested abacus beads", - "A salty bandana soaked in salty seawater and ink", - "A weather-beaten satchel full of crumpled grid paper", - "A rusty whisker arrangement forming a perfect sine wave", - "A salty snout with geometry scars", - "A cyber-pirate bandana soaked in salty seawater and ink", - "A ancient waistcoat buttoned with old copper coins", - "A rusty tricorn hat with nested abacus beads", - "A rusty eyepatch displaying coordinate maps" - ], - "smell": [ - "reminiscent of cheese and damp wood", - "suspiciously smelling of ink and sour lemons", - "suspiciously smelling of ink and spilled ink", - "pleasantly smelling of rum and damp wood", - "faintly of seaweed and damp wood", - "faintly of pine tar and sour lemons", - "faintly of ink and lightning sparks", - "like a mix of pine tar and spilled ink", - "strongly of ozone and sour lemons", - "like a mix of ozone and salty air", - "suspiciously smelling of seaweed and rusty iron", - "suspiciously smelling of seaweed and spilled ink", - "faintly of cheese and sour lemons", - "like a mix of ink and lightning sparks", - "strongly of gunpowder and spilled ink", - "pleasantly smelling of seaweed and peppermint oil", - "suspiciously smelling of rum and lightning sparks", - "suspiciously smelling of ozone and stale grog", - "pleasantly smelling of rum and burnt abacuses", - "strongly of cheese and damp wood", - "Ozone sparks and sulfur", - "reminiscent of gunpowder and wet tail fur", - "strongly of cheese and burnt abacuses", - "like a mix of pine tar and wet tail fur", - "strongly of cheese and spilled ink", - "like a mix of ozone and damp wood", - "pleasantly smelling of ink and sour lemons", - "pleasantly smelling of copper and wet tail fur", - "faintly of rum and burnt abacuses", - "suspiciously smelling of gunpowder and rusty iron", - "like a mix of copper and lightning sparks", - "pleasantly smelling of seaweed and burnt abacuses", - "reminiscent of copper and lightning sparks", - "reminiscent of cheese and burnt abacuses", - "pleasantly smelling of parchment and burnt abacuses", - "like a mix of seaweed and damp wood", - "Gunpowder smoke and melted candle wax", - "like a mix of ozone and burnt abacuses", - "faintly of rum and stale grog", - "like a mix of cheese and spilled ink", - "suspiciously smelling of parchment and sour lemons", - "faintly of copper and wet tail fur", - "faintly of cheese and wet tail fur", - "faintly of cheese and damp wood", - "faintly of pine tar and rusty iron", - "strongly of gunpowder and peppermint oil", - "faintly of pine tar and damp wood", - "strongly of seaweed and damp wood", - "like a mix of copper and wet tail fur", - "suspiciously smelling of cheese and salty air", - "faintly of rum and lightning sparks", - "reminiscent of chalk dust and spilled ink", - "strongly of copper and wet tail fur", - "pleasantly smelling of pine tar and damp wood", - "Damp sea charts and ozone", - "like a mix of cheese and stale grog", - "strongly of ink and rusty iron", - "like a mix of seaweed and salty air", - "pleasantly smelling of chalk dust and spilled ink", - "pleasantly smelling of chalk dust and peppermint oil", - "suspiciously smelling of copper and salty air", - "reminiscent of ozone and wet tail fur", - "faintly of rum and salty air", - "reminiscent of chalk dust and sour lemons", - "suspiciously smelling of pine tar and spilled ink", - "like a mix of cheese and wet tail fur", - "strongly of ink and sour lemons", - "suspiciously smelling of chalk dust and damp wood", - "Burnt tea leaves and old books", - "faintly of parchment and wet tail fur", - "faintly of copper and sour lemons", - "faintly of chalk dust and spilled ink", - "faintly of gunpowder and lightning sparks", - "reminiscent of parchment and wet tail fur", - "pleasantly smelling of parchment and rusty iron", - "reminiscent of cheese and stale grog", - "reminiscent of ink and wet tail fur", - "pleasantly smelling of gunpowder and burnt abacuses", - "reminiscent of parchment and stale grog", - "pleasantly smelling of chalk dust and lightning sparks", - "reminiscent of copper and peppermint oil", - "pleasantly smelling of ink and peppermint oil", - "Ozone and seaweed-wrapped tobacco", - "strongly of cheese and rusty iron", - "faintly of chalk dust and burnt abacuses", - "pleasantly smelling of seaweed and lightning sparks", - "suspiciously smelling of rum and stale grog", - "like a mix of rum and burnt abacuses", - "suspiciously smelling of rum and peppermint oil", - "suspiciously smelling of copper and burnt abacuses", - "reminiscent of pine tar and spilled ink", - "reminiscent of parchment and lightning sparks", - "suspiciously smelling of ink and rusty iron", - "like a mix of pine tar and damp wood", - "pleasantly smelling of chalk dust and salty air", - "faintly of copper and burnt abacuses", - "pleasantly smelling of ozone and stale grog", - "like a mix of cheese and lightning sparks", - "strongly of pine tar and salty air", - "like a mix of seaweed and rusty iron", - "pleasantly smelling of seaweed and sour lemons", - "suspiciously smelling of gunpowder and lightning sparks", - "suspiciously smelling of ink and peppermint oil", - "like a mix of seaweed and wet tail fur", - "reminiscent of gunpowder and burnt abacuses", - "suspiciously smelling of pine tar and sour lemons", - "pleasantly smelling of ink and lightning sparks", - "reminiscent of pine tar and peppermint oil", - "pleasantly smelling of rum and rusty iron", - "like a mix of gunpowder and wet tail fur", - "suspiciously smelling of pine tar and lightning sparks", - "pleasantly smelling of copper and spilled ink", - "strongly of ozone and wet tail fur", - "reminiscent of ink and burnt abacuses", - "like a mix of copper and stale grog", - "faintly of copper and stale grog", - "like a mix of copper and peppermint oil", - "suspiciously smelling of copper and stale grog", - "suspiciously smelling of cheese and burnt abacuses", - "strongly of ozone and spilled ink", - "Burnt cheese and gunpowder", - "pleasantly smelling of seaweed and salty air", - "pleasantly smelling of seaweed and stale grog", - "like a mix of ozone and peppermint oil", - "strongly of pine tar and rusty iron", - "strongly of gunpowder and stale grog", - "reminiscent of parchment and rusty iron", - "faintly of copper and lightning sparks", - "faintly of chalk dust and sour lemons", - "suspiciously smelling of chalk dust and stale grog", - "Freshly sharpened pencils and vinegar", - "pleasantly smelling of parchment and sour lemons", - "pleasantly smelling of ozone and sour lemons", - "faintly of cheese and rusty iron", - "like a mix of seaweed and burnt abacuses", - "reminiscent of gunpowder and damp wood", - "like a mix of cheese and sour lemons", - "reminiscent of seaweed and stale grog", - "like a mix of gunpowder and peppermint oil", - "reminiscent of ozone and stale grog", - "strongly of seaweed and burnt abacuses", - "faintly of ink and rusty iron", - "faintly of ink and spilled ink", - "suspiciously smelling of pine tar and peppermint oil", - "strongly of parchment and lightning sparks", - "reminiscent of chalk dust and damp wood", - "suspiciously smelling of gunpowder and stale grog", - "faintly of ozone and lightning sparks", - "faintly of rum and spilled ink", - "strongly of pine tar and lightning sparks", - "strongly of pine tar and burnt abacuses", - "pleasantly smelling of rum and spilled ink", - "reminiscent of rum and stale grog", - "faintly of copper and rusty iron", - "suspiciously smelling of ozone and wet tail fur", - "strongly of seaweed and spilled ink", - "suspiciously smelling of parchment and salty air", - "strongly of copper and lightning sparks", - "pleasantly smelling of cheese and peppermint oil", - "reminiscent of cheese and peppermint oil", - "pleasantly smelling of seaweed and rusty iron", - "suspiciously smelling of ozone and lightning sparks", - "strongly of rum and peppermint oil", - "faintly of chalk dust and peppermint oil", - "like a mix of rum and wet tail fur", - "like a mix of cheese and damp wood", - "reminiscent of chalk dust and wet tail fur", - "reminiscent of rum and peppermint oil", - "suspiciously smelling of copper and spilled ink", - "reminiscent of cheese and sour lemons", - "pleasantly smelling of parchment and damp wood", - "reminiscent of chalk dust and salty air", - "pleasantly smelling of pine tar and salty air", - "strongly of rum and wet tail fur", - "strongly of ink and damp wood", - "like a mix of pine tar and peppermint oil", - "strongly of gunpowder and sour lemons", - "faintly of ink and burnt abacuses", - "like a mix of rum and sour lemons", - "pleasantly smelling of cheese and sour lemons", - "strongly of cheese and lightning sparks", - "reminiscent of chalk dust and burnt abacuses", - "Salty parchment and copper coins", - "strongly of rum and lightning sparks", - "suspiciously smelling of seaweed and burnt abacuses", - "reminiscent of cheese and rusty iron", - "faintly of cheese and lightning sparks", - "like a mix of cheese and peppermint oil", - "reminiscent of ink and spilled ink", - "faintly of pine tar and peppermint oil", - "faintly of parchment and burnt abacuses", - "reminiscent of parchment and burnt abacuses", - "strongly of pine tar and peppermint oil", - "pleasantly smelling of ozone and peppermint oil", - "like a mix of parchment and rusty iron", - "faintly of ink and stale grog", - "pleasantly smelling of pine tar and burnt abacuses", - "suspiciously smelling of cheese and peppermint oil", - "strongly of gunpowder and rusty iron", - "reminiscent of cheese and spilled ink" - ], - "first_words": [ - "By the deep sea! the probability indicates we are sinking logarithmically!", - "I have transformed... and my hunger is logarithmic!", - "Avast! the geometry of this deck requires the coordinates of the treasure island!", - "Blow me down! the geometry of this deck requires the shortest boarding vector!", - "Blast it! the slope of the sea shows a prime number of hazards!", - "Aha! the abacus predicts infinite cheese!", - "Blow me down! the probability indicates the shortest boarding vector!", - "Arrr, the hypotenuse is magnificent!", - "To the brig! the slope of the sea shows a prime number of hazards!", - "Shiver my timbers! the coordinates point to the gats are ready to fire!", - "Arrr! my tail calculations reveal a division by zero!", - "Avast! the slope of the sea shows the coordinates of the treasure island!", - "Arrr! the tangent of the gat implies a division by zero!", - "Shiver my timbers! the slope of the sea shows an escape velocity of ten knots!", - "Hear me crew! the slope of the sea shows a division by zero!", - "Shiver my timbers! the probability indicates a division by zero!", - "Hear me crew! the captain's slide rule shows infinite cheese!", - "Avast! the geometry of this deck requires the gats are ready to fire!", - "Hear me crew! the tangent of the gat implies a perfect circle of doom!", - "Aha! the formula for cheese says we are sinking logarithmically!", - "Aha! the tangent of the gat implies a jackpot of gold doubloons!", - "Aha! the probability indicates infinite cheese!", - "To the brig! my tail calculations reveal a division by zero!", - "To the brig! the tangent of the gat implies a jackpot of gold doubloons!", - "Look alive! the slope of the sea shows infinite cheese!", - "Look alive! the geometry of this deck requires a perfect circle of doom!", - "Shiver my timbers! the captain's slide rule shows we are sinking logarithmically!", - "Blast it! the tangent of the gat implies the shortest boarding vector!", - "To the horizon, at a 45-degree angle!", - "To the brig! the formula for cheese says the gats are ready to fire!", - "Arrr! the captain's slide rule shows the shortest boarding vector!", - "Blow me down! the coordinates point to an escape velocity of ten knots!", - "Hear me crew! the probability indicates we are sinking logarithmically!", - "Look alive! the matrix of the hold contains a jackpot of gold doubloons!", - "Blow me down! the slope of the sea shows an escape velocity of ten knots!", - "Aha! the slope of the sea shows infinite cheese!", - "Arrr, divide by zero!", - "Arrr! the captain's slide rule shows infinite cheese!", - "Arrr! the coordinates point to the coordinates of the treasure island!", - "Look alive! my tail calculations reveal we are sinking logarithmically!", - "To the brig! the slope of the sea shows an escape velocity of ten knots!", - "Blast it! the coordinates point to an escape velocity of ten knots!", - "Aha! the matrix of the hold contains we are sinking logarithmically!", - "Look alive! the coordinates point to the gats are ready to fire!", - "Arrr! the geometry of this deck requires the coordinates of the treasure island!", - "Blast it! the slope of the sea shows an escape velocity of ten knots!", - "Arrr! the slope of the sea shows a jackpot of gold doubloons!", - "By the deep sea! the probability indicates the shortest boarding vector!", - "Hear me crew! the coordinates point to an escape velocity of ten knots!", - "Look alive! the geometry of this deck requires infinite cheese!", - "Arrr! the abacus predicts infinite cheese!", - "Arrr! the slope of the sea shows infinite cheese!", - "Avast! the probability indicates the shortest boarding vector!", - "Avast! the abacus predicts a prime number of hazards!", - "Blow me down! the slope of the sea shows a perfect circle of doom!", - "Blow me down! the formula for cheese says the coordinates of the treasure island!", - "Hear me crew! the coordinates point to the coordinates of the treasure island!", - "To the brig! the captain's slide rule shows the coordinates of the treasure island!", - "Hear me crew! my tail calculations reveal a prime number of hazards!", - "Aha! the tangent of the gat implies a division by zero!", - "Arrr! the geometry of this deck requires a jackpot of gold doubloons!", - "Blow me down! my tail calculations reveal a jackpot of gold doubloons!", - "To the brig! the coordinates point to we are sinking logarithmically!", - "Blow me down! the geometry of this deck requires we are sinking logarithmically!", - "By the deep sea! the slope of the sea shows the shortest boarding vector!", - "Hear me crew! the formula for cheese says infinite cheese!", - "Shiver my timbers! the coordinates point to a prime number of hazards!", - "Shiver my timbers! the matrix of the hold contains the coordinates of the treasure island!", - "Aha! the formula for cheese says the shortest boarding vector!", - "Avast! the slope of the sea shows we are sinking logarithmically!", - "Aha! my tail calculations reveal the shortest boarding vector!", - "By the deep sea! the matrix of the hold contains the coordinates of the treasure island!", - "Arrr! the matrix of the hold contains a prime number of hazards!", - "Shiver my timbers! my tail calculations reveal a perfect circle of doom!", - "Arrr! the geometry of this deck requires we are sinking logarithmically!", - "Hear me crew! the slope of the sea shows a perfect circle of doom!", - "Aha! the matrix of the hold contains an escape velocity of ten knots!", - "Shiver my timbers! the captain's slide rule shows the gats are ready to fire!", - "Blow me down! the captain's slide rule shows the coordinates of the treasure island!", - "Hear me crew! the coordinates point to the shortest boarding vector!", - "Blast it! the captain's slide rule shows the shortest boarding vector!", - "Arrr! the coordinates point to the shortest boarding vector!", - "Avast! the geometry of this deck requires a perfect circle of doom!", - "Arrr! the coordinates point to infinite cheese!", - "Arrr! the tangent of the gat implies infinite cheese!", - "Look alive! the slope of the sea shows a jackpot of gold doubloons!", - "Avast! the slope of the sea shows a division by zero!", - "Blow me down! the coordinates point to the shortest boarding vector!", - "Shiver my timbers! the geometry of this deck requires a division by zero!", - "Arrr! the formula for cheese says we are sinking logarithmically!", - "Blast it! the geometry of this deck requires the coordinates of the treasure island!", - "Look alive! the matrix of the hold contains a division by zero!", - "To the brig! the captain's slide rule shows the gats are ready to fire!", - "Hear me crew! the probability indicates an escape velocity of ten knots!", - "Avast! the tangent of the gat implies a perfect circle of doom!", - "Shiver my timbers! the captain's slide rule shows infinite cheese!", - "Shiver my decimals!", - "Blast it! the probability indicates an escape velocity of ten knots!", - "Arrr! the probability indicates the gats are ready to fire!", - "Arrr! the captain's slide rule shows the coordinates of the treasure island!", - "Blow me down! the tangent of the gat implies the shortest boarding vector!", - "Shiver my timbers! the matrix of the hold contains we are sinking logarithmically!", - "By the deep sea! the abacus predicts we are sinking logarithmically!", - "Avast! the slope of the sea shows an escape velocity of ten knots!", - "Aha! the tangent of the gat implies an escape velocity of ten knots!", - "Blow me down! the tangent of the gat implies an escape velocity of ten knots!", - "Avast! the probability indicates we are sinking logarithmically!", - "Aha! the slope of the sea shows a perfect circle of doom!", - "Look alive! my tail calculations reveal the coordinates of the treasure island!", - "Shiver my timbers! the formula for cheese says infinite cheese!", - "Aha! the matrix of the hold contains the shortest boarding vector!", - "Aha! the geometry of this deck requires the shortest boarding vector!", - "Blast it! the probability indicates a division by zero!", - "To the brig! the formula for cheese says a division by zero!", - "Shiver my timbers! the slope of the sea shows a jackpot of gold doubloons!", - "By the deep sea! the tangent of the gat implies a perfect circle of doom!", - "By the deep sea! the formula for cheese says we are sinking logarithmically!", - "Blow me down! the matrix of the hold contains a division by zero!", - "Blow me down! the captain's slide rule shows an escape velocity of ten knots!", - "Arrr! the geometry of this deck requires a division by zero!", - "Blow me down! the abacus predicts a perfect circle of doom!", - "Avast! the matrix of the hold contains a jackpot of gold doubloons!", - "To the brig! my tail calculations reveal a prime number of hazards!", - "Aha! the geometry of this deck requires we are sinking logarithmically!", - "Hear me crew! the tangent of the gat implies a division by zero!", - "Hear me crew! the abacus predicts a jackpot of gold doubloons!", - "Arrr! my tail calculations reveal a prime number of hazards!", - "By the deep sea! the probability indicates a jackpot of gold doubloons!", - "By the deep sea! the formula for cheese says the shortest boarding vector!", - "By the deep sea! the geometry of this deck requires the gats are ready to fire!", - "Hear me crew! the abacus predicts a division by zero!", - "By the deep sea! the tangent of the gat implies an escape velocity of ten knots!", - "Blast it! the probability indicates we are sinking logarithmically!", - "Hear me crew! the geometry of this deck requires infinite cheese!", - "Avast! the captain's slide rule shows the shortest boarding vector!", - "To the brig! the slope of the sea shows a division by zero!", - "Hear me crew! the captain's slide rule shows the gats are ready to fire!", - "Avast! the tangent of the gat implies a jackpot of gold doubloons!", - "Hear me crew! my tail calculations reveal the shortest boarding vector!", - "Shiver my timbers! the formula for cheese says the coordinates of the treasure island!", - "Hear me crew! the matrix of the hold contains a perfect circle of doom!", - "By the deep sea! the geometry of this deck requires the coordinates of the treasure island!", - "Look alive! the formula for cheese says the coordinates of the treasure island!", - "Gats, cheese, and geometry\u2014that's all a rat needs!", - "Arrr! the probability indicates a jackpot of gold doubloons!", - "Hear me crew! the probability indicates a prime number of hazards!", - "Blow me down! the geometry of this deck requires an escape velocity of ten knots!", - "To the brig! the matrix of the hold contains a division by zero!", - "Blast it! my tail calculations reveal infinite cheese!", - "Shiver my timbers! my tail calculations reveal a jackpot of gold doubloons!", - "Shiver my timbers! the matrix of the hold contains the gats are ready to fire!", - "Hear me crew! the abacus predicts the gats are ready to fire!", - "Avast! the geometry of this deck requires we are sinking logarithmically!", - "Shiver my timbers! the tangent of the gat implies we are sinking logarithmically!", - "Avast! the tangent of the gat implies a prime number of hazards!", - "Avast! my tail calculations reveal infinite cheese!", - "Arrr! the matrix of the hold contains the shortest boarding vector!", - "Blow me down! the slope of the sea shows a prime number of hazards!", - "Aha! the coordinates point to a division by zero!", - "To the brig! the matrix of the hold contains we are sinking logarithmically!", - "Look alive! the formula for cheese says we are sinking logarithmically!", - "Look alive! the abacus predicts a division by zero!", - "By the deep sea! the coordinates point to an escape velocity of ten knots!", - "Blow me down! the coordinates point to we are sinking logarithmically!", - "By the deep sea! the probability indicates the coordinates of the treasure island!", - "To the brig! the formula for cheese says a prime number of hazards!", - "To the brig! the probability indicates infinite cheese!", - "Look alive! the tangent of the gat implies an escape velocity of ten knots!", - "Shiver my timbers! the probability indicates we are sinking logarithmically!", - "By the deep sea! the abacus predicts infinite cheese!", - "Blow me down! the matrix of the hold contains the coordinates of the treasure island!", - "Arrr! my tail calculations reveal a perfect circle of doom!", - "Avast! the captain's slide rule shows an escape velocity of ten knots!", - "Arrr! the captain's slide rule shows an escape velocity of ten knots!", - "Blast it! the abacus predicts a prime number of hazards!", - "Blast it! my tail calculations reveal a division by zero!", - "Blow me down! the probability indicates a jackpot of gold doubloons!", - "By the deep sea! the coordinates point to a division by zero!", - "By the deep sea! the abacus predicts a jackpot of gold doubloons!", - "Blast it! the captain's slide rule shows a prime number of hazards!", - "Aha! the slope of the sea shows the coordinates of the treasure island!", - "To the brig! the matrix of the hold contains a jackpot of gold doubloons!", - "Look alive! the tangent of the gat implies the gats are ready to fire!", - "Blast it! the matrix of the hold contains a prime number of hazards!", - "Arrr! the tangent of the gat implies a perfect circle of doom!", - "Avast! the slope of the sea shows a jackpot of gold doubloons!", - "Shiver my timbers! the formula for cheese says a prime number of hazards!", - "By the deep sea! my tail calculations reveal we are sinking logarithmically!", - "Hear me crew! the slope of the sea shows a jackpot of gold doubloons!", - "Aha! the geometry of this deck requires infinite cheese!", - "To the brig! the geometry of this deck requires the coordinates of the treasure island!", - "Look alive! the coordinates point to the coordinates of the treasure island!", - "Look alive! the formula for cheese says the shortest boarding vector!", - "By the deep sea! the geometry of this deck requires a prime number of hazards!", - "To the brig! the tangent of the gat implies we are sinking logarithmically!", - "Hear me crew! the coordinates point to a perfect circle of doom!", - "Aha! the formula for cheese says infinite cheese!", - "By the deep sea! the formula for cheese says a prime number of hazards!", - "Aha! the matrix of the hold contains the gats are ready to fire!", - "Arrr! my tail calculations reveal the gats are ready to fire!" - ], - "good_at_math": [ - "Thinks they are, they uses slide rules to navigate stormy seas but gets confused by negative numbers.", - "Thinks they are, they remembers the first 100 digits of Pi and has the abacus tattoos to prove it.", - "Yes, they understands the probability of drawing a Joker but gets confused by negative numbers.", - "According to the abacus, they can balance the captain's ledgers in their head but gets confused by negative numbers.", - "No, they can solve equations under threat of cannon fire but gets distracted by shiny brass coins.", - "No, they knows how to divide cheese by zero and has the abacus tattoos to prove it.", - "In theory, they can solve equations under threat of cannon fire but gets distracted by shiny brass coins.", - "On paper, they can solve equations under threat of cannon fire but only when drunk on grog.", - "Absolutely, they remembers the first 100 digits of Pi and has a certificate from the Stowaway Academy.", - "No, they understands the geometry of boarding leaps and has the abacus tattoos to prove it.", - "Thinks they are, they can solve equations under threat of cannon fire but refuses to use prime numbers.", - "Barely, they can calculate the trajectory of a gat bullet but refuses to use prime numbers.", - "Only, they understands the geometry of boarding leaps but only when drunk on grog.", - "Absolutely, they can solve equations under threat of cannon fire but gets confused by negative numbers.", - "Thinks they are, they understands the geometry of boarding leaps but only when cheese is involved.", - "Thinks they are, they knows how to divide cheese by zero but only when cheese is involved.", - "On paper, they remembers the first 100 digits of Pi but only in base-3.", - "On paper, they knows how to divide cheese by zero and has a certificate from the Stowaway Academy.", - "According to the abacus, they uses slide rules to navigate stormy seas and has the abacus tattoos to prove it.", - "Yes, they can count abacus beads in pitch black hold but only when cheese is involved.", - "No, they can calculate the trajectory of a gat bullet but gets confused by negative numbers.", - "No, they interprets sea maps as coordinate grids but thinks fractions are bad luck.", - "Secretly, they can solve equations under threat of cannon fire but thinks fractions are bad luck.", - "On paper, they uses slide rules to navigate stormy seas and has a certificate from the Stowaway Academy.", - "According to the abacus, they interprets sea maps as coordinate grids but only in base-3.", - "In theory, they interprets sea maps as coordinate grids but gets distracted by shiny brass coins.", - "Thinks they are, they knows how to divide cheese by zero but thinks fractions are bad luck.", - "Secretly, they can balance the captain's ledgers in their head but only when cheese is involved.", - "Only, they interprets sea maps as coordinate grids and has the abacus tattoos to prove it.", - "According to the abacus, they can balance the captain's ledgers in their head but gets distracted by shiny brass coins.", - "Only, they uses slide rules to navigate stormy seas but refuses to use prime numbers.", - "No, they remembers the first 100 digits of Pi but only when drunk on grog.", - "On paper, they can calculate the trajectory of a gat bullet but gets confused by negative numbers.", - "In theory, they uses slide rules to navigate stormy seas but thinks fractions are bad luck.", - "Thinks they are, they understands the geometry of boarding leaps but refuses to use prime numbers.", - "Absolutely, they can count abacus beads in pitch black hold but gets confused by negative numbers.", - "Yes, they can solve equations under threat of cannon fire but only when drunk on grog.", - "Only, they understands the geometry of boarding leaps and has the abacus tattoos to prove it.", - "Thinks they are, they can balance the captain's ledgers in their head but refuses to use prime numbers.", - "Secretly, they can solve equations under threat of cannon fire but gets confused by negative numbers.", - "No, they can calculate the trajectory of a gat bullet and has the abacus tattoos to prove it.", - "Yes, specializes in escape vector geometry.", - "Secretly, they knows how to divide cheese by zero but only when drunk on grog.", - "No, they understands the geometry of boarding leaps and has a certificate from the Stowaway Academy.", - "Absolutely, they understands the probability of drawing a Joker and has the abacus tattoos to prove it.", - "Absolutely, they interprets sea maps as coordinate grids but only in base-3.", - "Yes, they can calculate the trajectory of a gat bullet but only when cheese is involved.", - "In theory, they can balance the captain's ledgers in their head but gets confused by negative numbers.", - "No, they uses slide rules to navigate stormy seas but refuses to use prime numbers.", - "According to the abacus, they can solve equations under threat of cannon fire but gets distracted by shiny brass coins.", - "Absolutely, they remembers the first 100 digits of Pi but gets confused by negative numbers.", - "Thinks they are, they uses slide rules to navigate stormy seas but only when drunk on grog.", - "Yes, they can count abacus beads in pitch black hold but only in base-3.", - "Secretly, they can calculate the trajectory of a gat bullet but only when drunk on grog.", - "On paper, they can calculate the trajectory of a gat bullet but only when drunk on grog.", - "Only, they can solve equations under threat of cannon fire but gets confused by negative numbers.", - "Yes, they can count abacus beads in pitch black hold and has the abacus tattoos to prove it.", - "In theory, they can balance the captain's ledgers in their head but only when cheese is involved.", - "Barely, they interprets sea maps as coordinate grids but thinks fractions are bad luck.", - "According to the abacus, they interprets sea maps as coordinate grids and claims numbers speak to them in dreams.", - "Thinks they are, they can balance the captain's ledgers in their head but thinks fractions are bad luck.", - "Absolutely, they knows how to divide cheese by zero but refuses to use prime numbers.", - "Secretly, they understands the geometry of boarding leaps but only when drunk on grog.", - "In theory, they can calculate the trajectory of a gat bullet but only in base-3.", - "Barely, they knows how to divide cheese by zero and has a certificate from the Stowaway Academy.", - "Absolutely, they remembers the first 100 digits of Pi but gets distracted by shiny brass coins.", - "Thinks they are, they understands the geometry of boarding leaps but thinks fractions are bad luck.", - "On paper, they remembers the first 100 digits of Pi but gets confused by negative numbers.", - "On paper, they understands the probability of drawing a Joker but gets distracted by shiny brass coins.", - "Secretly, they can solve equations under threat of cannon fire but only when drunk on grog.", - "In theory, they interprets sea maps as coordinate grids and has the abacus tattoos to prove it.", - "In theory, they knows how to divide cheese by zero and has the abacus tattoos to prove it.", - "Absolutely, they knows how to divide cheese by zero but only in base-3.", - "Only, they remembers the first 100 digits of Pi but only when cheese is involved.", - "Only, they understands the probability of drawing a Joker but only when cheese is involved.", - "Barely, they can balance the captain's ledgers in their head but gets distracted by shiny brass coins.", - "No, they understands the probability of drawing a Joker and has a certificate from the Stowaway Academy.", - "In theory, they remembers the first 100 digits of Pi but only when cheese is involved.", - "Yes, they can count abacus beads in pitch black hold but only when drunk on grog.", - "In theory, they can calculate the trajectory of a gat bullet and claims numbers speak to them in dreams.", - "Only, they understands the probability of drawing a Joker and has a certificate from the Stowaway Academy.", - "Secretly, they understands the geometry of boarding leaps and has a certificate from the Stowaway Academy.", - "Secretly, they remembers the first 100 digits of Pi but only when cheese is involved.", - "Absolutely, they understands the geometry of boarding leaps and has a certificate from the Stowaway Academy.", - "Thinks they are, they understands the probability of drawing a Joker and claims numbers speak to them in dreams.", - "On paper, they understands the probability of drawing a Joker but only when cheese is involved.", - "Only, they understands the geometry of boarding leaps and has a certificate from the Stowaway Academy.", - "Thinks they are, they remembers the first 100 digits of Pi but thinks fractions are bad luck.", - "No, they understands the probability of drawing a Joker but thinks fractions are bad luck.", - "On paper, they can calculate the trajectory of a gat bullet but thinks fractions are bad luck.", - "In theory, they can calculate the trajectory of a gat bullet but only when drunk on grog.", - "Yes, but only in base-8 because they lost two claws to a cat.", - "No, they uses slide rules to navigate stormy seas but gets distracted by shiny brass coins.", - "Only, they uses slide rules to navigate stormy seas and has a certificate from the Stowaway Academy.", - "Thinks they are, they can count abacus beads in pitch black hold but gets confused by negative numbers.", - "Absolutely, they understands the probability of drawing a Joker but gets confused by negative numbers.", - "Secretly, they can calculate the trajectory of a gat bullet but only in base-3.", - "In theory, they interprets sea maps as coordinate grids but only in base-3.", - "Absolutely, they understands the probability of drawing a Joker but refuses to use prime numbers.", - "Secretly, they remembers the first 100 digits of Pi and claims numbers speak to them in dreams.", - "Thinks they are, they can count abacus beads in pitch black hold and has a certificate from the Stowaway Academy.", - "Secretly, they uses slide rules to navigate stormy seas and has the abacus tattoos to prove it.", - "Absolutely, they interprets sea maps as coordinate grids and has the abacus tattoos to prove it.", - "Only, they understands the geometry of boarding leaps but gets distracted by shiny brass coins.", - "Secretly, they uses slide rules to navigate stormy seas but only in base-3.", - "According to the abacus, they knows how to divide cheese by zero but gets confused by negative numbers.", - "According to the abacus, they interprets sea maps as coordinate grids and has the abacus tattoos to prove it.", - "No, they understands the probability of drawing a Joker and claims numbers speak to them in dreams.", - "No, they interprets sea maps as coordinate grids and has the abacus tattoos to prove it.", - "According to the abacus, they understands the probability of drawing a Joker but only in base-3.", - "Thinks they are, they can calculate the trajectory of a gat bullet but gets confused by negative numbers.", - "Absolutely, they remembers the first 100 digits of Pi and claims numbers speak to them in dreams.", - "Secretly, they interprets sea maps as coordinate grids and has a certificate from the Stowaway Academy.", - "Absolutely, they can solve equations under threat of cannon fire and has a certificate from the Stowaway Academy.", - "Yes, they understands the probability of drawing a Joker but thinks fractions are bad luck.", - "Absolutely, they can count abacus beads in pitch black hold and has the abacus tattoos to prove it.", - "Absolutely, they uses slide rules to navigate stormy seas and has the abacus tattoos to prove it.", - "According to the abacus, they understands the geometry of boarding leaps and claims numbers speak to them in dreams.", - "Only, they can solve equations under threat of cannon fire and has a certificate from the Stowaway Academy.", - "Absolutely, they understands the geometry of boarding leaps but gets confused by negative numbers.", - "Absolutely, they understands the geometry of boarding leaps but gets distracted by shiny brass coins.", - "Absolutely, they interprets sea maps as coordinate grids but only when cheese is involved.", - "No, they can balance the captain's ledgers in their head but gets confused by negative numbers.", - "Absolutely, they can solve equations under threat of cannon fire but refuses to use prime numbers.", - "No, they can solve equations under threat of cannon fire but thinks fractions are bad luck.", - "Barely, they uses slide rules to navigate stormy seas but only in base-3.", - "In theory, they knows how to divide cheese by zero but only in base-3.", - "Thinks they are, they can solve equations under threat of cannon fire but only when drunk on grog.", - "Absolutely, they uses slide rules to navigate stormy seas but only when drunk on grog.", - "Only, they understands the probability of drawing a Joker but only when drunk on grog.", - "They can compute a Fibonacci sequence in under three seconds.", - "Secretly, they knows how to divide cheese by zero and claims numbers speak to them in dreams.", - "Thinks they are, they can count abacus beads in pitch black hold but gets distracted by shiny brass coins.", - "Secretly, they interprets sea maps as coordinate grids and claims numbers speak to them in dreams.", - "Absolutely, they can solve equations under threat of cannon fire but only when drunk on grog.", - "Yes, they can balance the captain's ledgers in their head but gets distracted by shiny brass coins.", - "Only, they knows how to divide cheese by zero and has the abacus tattoos to prove it.", - "Thinks they are, they can calculate the trajectory of a gat bullet but only when cheese is involved.", - "According to the abacus, they can calculate the trajectory of a gat bullet and has the abacus tattoos to prove it.", - "In theory, they remembers the first 100 digits of Pi but gets distracted by shiny brass coins.", - "According to the abacus, they can balance the captain's ledgers in their head but thinks fractions are bad luck.", - "No, they understands the probability of drawing a Joker but only in base-3.", - "In theory, they uses slide rules to navigate stormy seas but gets confused by negative numbers.", - "Thinks they are, they uses slide rules to navigate stormy seas but refuses to use prime numbers.", - "Secretly, they interprets sea maps as coordinate grids but only when drunk on grog.", - "Yes, they interprets sea maps as coordinate grids but gets confused by negative numbers.", - "No, they understands the geometry of boarding leaps but only in base-3.", - "Secretly, they remembers the first 100 digits of Pi but thinks fractions are bad luck.", - "Absolutely, they can count abacus beads in pitch black hold but refuses to use prime numbers.", - "In theory, they uses slide rules to navigate stormy seas and has the abacus tattoos to prove it.", - "Yes, they can count abacus beads in pitch black hold but gets confused by negative numbers.", - "Yes, they can solve equations under threat of cannon fire but only in base-3.", - "Absolutely, they can balance the captain's ledgers in their head and has a certificate from the Stowaway Academy.", - "Yes, they can balance the captain's ledgers in their head and claims numbers speak to them in dreams.", - "Absolutely, they can calculate the trajectory of a gat bullet and claims numbers speak to them in dreams.", - "On paper, they knows how to divide cheese by zero but thinks fractions are bad luck.", - "Thinks they are, they understands the probability of drawing a Joker but only when cheese is involved.", - "Thinks they are, they uses slide rules to navigate stormy seas but only in base-3.", - "Barely, they understands the probability of drawing a Joker but gets distracted by shiny brass coins.", - "Only, they understands the geometry of boarding leaps but gets confused by negative numbers.", - "On paper, they knows how to divide cheese by zero but only when drunk on grog.", - "On paper, they can count abacus beads in pitch black hold but gets confused by negative numbers.", - "No, they understands the probability of drawing a Joker but only when cheese is involved.", - "Barely, they knows how to divide cheese by zero but refuses to use prime numbers.", - "On paper, they understands the geometry of boarding leaps but thinks fractions are bad luck.", - "In theory, they can balance the captain's ledgers in their head but gets distracted by shiny brass coins.", - "Yes, they remembers the first 100 digits of Pi but only when cheese is involved.", - "Only, they knows how to divide cheese by zero but only when cheese is involved.", - "Only, they can balance the captain's ledgers in their head but only when cheese is involved.", - "Absolutely, they can count abacus beads in pitch black hold and has a certificate from the Stowaway Academy.", - "Barely, they can count abacus beads in pitch black hold but gets confused by negative numbers.", - "On paper, they can count abacus beads in pitch black hold and claims numbers speak to them in dreams.", - "Absolutely, they understands the geometry of boarding leaps and claims numbers speak to them in dreams.", - "Barely, they knows how to divide cheese by zero but only when cheese is involved.", - "Thinks they are, they can solve equations under threat of cannon fire but thinks fractions are bad luck.", - "In theory, they interprets sea maps as coordinate grids but refuses to use prime numbers.", - "Yes, they understands the probability of drawing a Joker but gets distracted by shiny brass coins.", - "In theory, they knows how to divide cheese by zero and claims numbers speak to them in dreams.", - "Yes, they interprets sea maps as coordinate grids and claims numbers speak to them in dreams.", - "Only, they remembers the first 100 digits of Pi and claims numbers speak to them in dreams.", - "Only, they uses slide rules to navigate stormy seas and has the abacus tattoos to prove it.", - "In theory, they can balance the captain's ledgers in their head but only in base-3.", - "On paper, they remembers the first 100 digits of Pi and has a certificate from the Stowaway Academy.", - "According to the abacus, they understands the geometry of boarding leaps but gets distracted by shiny brass coins.", - "On paper, they can solve equations under threat of cannon fire and has a certificate from the Stowaway Academy.", - "In theory, they understands the probability of drawing a Joker but refuses to use prime numbers.", - "Absolutely, they can balance the captain's ledgers in their head and has the abacus tattoos to prove it.", - "On paper, they can balance the captain's ledgers in their head but only when drunk on grog.", - "No, they can count abacus beads in pitch black hold but only when drunk on grog.", - "Barely, they uses slide rules to navigate stormy seas but only when drunk on grog.", - "No, they can balance the captain's ledgers in their head but thinks fractions are bad luck.", - "According to the abacus, they remembers the first 100 digits of Pi but refuses to use prime numbers.", - "Barely, they understands the probability of drawing a Joker but only when drunk on grog.", - "Yes, they can balance the captain's ledgers in their head but thinks fractions are bad luck.", - "Only, they can solve equations under threat of cannon fire but only when cheese is involved.", - "Absolutely, they interprets sea maps as coordinate grids and claims numbers speak to them in dreams.", - "On paper, they can calculate the trajectory of a gat bullet but gets distracted by shiny brass coins.", - "Barely, they can count abacus beads in pitch black hold and has the abacus tattoos to prove it.", - "Thinks they are, they interprets sea maps as coordinate grids but only when cheese is involved.", - "According to the abacus, they remembers the first 100 digits of Pi and claims numbers speak to them in dreams." - ], - "like": [ - "They secretly saves the gunpowder barrels during storm navigation.", - "They always shares the captain's stash with mathematical precision.", - "They faithfully guards the escape vectors when the seagulls attack.", - "They always shares the backup abacus even under heavy fire.", - "They cheerfully carries the coordinate charts when the seagulls attack.", - "They always shares the compass dial because they are a true friend.", - "They never steals the backup abacus to keep the crew happy.", - "They cleanly polishes the captain's stash because they are a true friend.", - "They secretly saves the gunpowder barrels just to be nice.", - "They cleanly polishes the gunpowder barrels during storm navigation.", - "They faithfully guards the compass dial during storm navigation.", - "They keep the gats polished and oiled.", - "They cleanly polishes the gunpowder barrels with mathematical precision.", - "They secretly saves the spare gats without asking for a share.", - "They secretly saves the gunpowder barrels to keep the crew happy.", - "They expertly calculates the captain's stash to keep the crew happy.", - "They secretly saves the escape vectors even under heavy fire.", - "They secretly saves the golden abacus beads to ensure a perfect escape.", - "They cheerfully carries the cheese wedges without asking for a share.", - "They never steals the cheese wedges with mathematical precision.", - "They quietly hums shanties for the captain's stash to ensure a perfect escape.", - "They quietly hums shanties for the captain's stash without asking for a share.", - "They always shares the compass dial to ensure a perfect escape.", - "They always shares the coordinate charts without asking for a share.", - "They never steals the captain's stash in base-10 code.", - "They expertly calculates the golden abacus beads without asking for a share.", - "They quietly hums shanties for the coordinate charts just to be nice.", - "They expertly calculates the cheese wedges when the seagulls attack.", - "They cheerfully carries the wet hold biscuits even under heavy fire.", - "They quietly hums shanties for the coordinate charts without asking for a share.", - "They boldly defends the gunpowder barrels without asking for a share.", - "They faithfully guards the coordinate charts in base-10 code.", - "They always shares the cheese wedges to keep the crew happy.", - "They always shares the cheese wedges with mathematical precision.", - "They never steals the gunpowder barrels without asking for a share.", - "They never steals the wet hold biscuits without asking for a share.", - "They expertly calculates the backup abacus in base-10 code.", - "They brilliantly solves the coordinate charts to ensure a perfect escape.", - "They expertly calculates the wet hold biscuits to ensure a perfect escape.", - "They cleanly polishes the escape vectors in base-10 code.", - "They secretly saves the gunpowder barrels to ensure a perfect escape.", - "They brilliantly solves the escape vectors even under heavy fire.", - "They cheerfully carries the gunpowder barrels because they are a true friend.", - "They boldly defends the gunpowder barrels because they are a true friend.", - "They boldly defends the compass dial in base-10 code.", - "They secretly saves the cheese wedges when the seagulls attack.", - "They quietly hums shanties for the compass dial with mathematical precision.", - "They cheerfully carries the gunpowder barrels without asking for a share.", - "They faithfully guards the wet hold biscuits with mathematical precision.", - "They quietly hums shanties for the compass dial when the seagulls attack.", - "They expertly calculates the coordinate charts when the seagulls attack.", - "They cleanly polishes the wet hold biscuits when the seagulls attack.", - "They boldly defends the compass dial to keep the crew happy.", - "They quietly hums shanties for the spare gats just to be nice.", - "They faithfully guards the cheese wedges even under heavy fire.", - "They cheerfully carries the spare gats because they are a true friend.", - "They faithfully guards the coordinate charts even under heavy fire.", - "They faithfully guards the compass dial with mathematical precision.", - "They cleanly polishes the wet hold biscuits with mathematical precision.", - "They quietly hums shanties for the captain's stash in base-10 code.", - "They never steals the coordinate charts with mathematical precision.", - "They cleanly polishes the gunpowder barrels even under heavy fire.", - "They cleanly polishes the spare gats even under heavy fire.", - "They boldly defends the escape vectors with mathematical precision.", - "They secretly saves the backup abacus when the seagulls attack.", - "They faithfully guards the coordinate charts when the seagulls attack.", - "They always shares the gunpowder barrels in base-10 code.", - "They never steals the captain's stash even under heavy fire.", - "They brilliantly solves the wet hold biscuits to ensure a perfect escape.", - "They faithfully guards the backup abacus because they are a true friend.", - "They expertly calculates the compass dial in base-10 code.", - "They secretly saves the backup abacus to ensure a perfect escape.", - "They expertly calculates the compass dial when the seagulls attack.", - "They brilliantly solves the backup abacus in base-10 code.", - "They expertly calculates the coordinate charts because they are a true friend.", - "They cleanly polishes the gunpowder barrels without asking for a share.", - "They expertly calculates the coordinate charts in base-10 code.", - "They expertly calculates the cheese wedges even under heavy fire.", - "They boldly defends the compass dial when the seagulls attack.", - "They cheerfully carries the wet hold biscuits during storm navigation.", - "They always shares the escape vectors because they are a true friend.", - "They cheerfully carries the escape vectors even under heavy fire.", - "They faithfully guards the captain's stash in base-10 code.", - "They boldly defends the compass dial because they are a true friend.", - "They faithfully guards the golden abacus beads even under heavy fire.", - "They cleanly polishes the golden abacus beads because they are a true friend.", - "They secretly saves the cheese wedges with mathematical precision.", - "They always shares the wet hold biscuits to keep the crew happy.", - "They quietly hums shanties for the spare gats without asking for a share.", - "They never steals the captain's stash to ensure a perfect escape.", - "They faithfully guards the gunpowder barrels because they are a true friend.", - "They quietly hums shanties for the wet hold biscuits with mathematical precision.", - "They never steals the cheese wedges when the seagulls attack.", - "They faithfully guards the compass dial to ensure a perfect escape.", - "They quietly hums shanties for the spare gats in base-10 code.", - "They never steals the compass dial even under heavy fire.", - "They faithfully guards the cheese wedges just to be nice.", - "They cleanly polishes the captain's stash when the seagulls attack.", - "They cleanly polishes the golden abacus beads just to be nice.", - "They expertly calculates the escape vectors even under heavy fire.", - "They never steals the compass dial when the seagulls attack.", - "They quietly hums shanties for the golden abacus beads when the seagulls attack.", - "They cheerfully carries the coordinate charts in base-10 code.", - "They faithfully guards the cheese wedges when the seagulls attack.", - "They secretly saves the wet hold biscuits without asking for a share.", - "They faithfully guards the compass dial because they are a true friend.", - "They always shares the backup abacus because they are a true friend.", - "They cleanly polishes the wet hold biscuits in base-10 code.", - "They boldly defends the backup abacus to ensure a perfect escape.", - "They cheerfully carries the spare gats even under heavy fire.", - "They brilliantly solves the coordinate charts with mathematical precision.", - "They secretly saves the compass dial because they are a true friend.", - "They faithfully guards the coordinate charts because they are a true friend.", - "They expertly calculates the backup abacus without asking for a share.", - "They never steals the coordinate charts during storm navigation.", - "They secretly saves the captain's stash when the seagulls attack.", - "They brilliantly solves the backup abacus just to be nice.", - "They boldly defends the gunpowder barrels to keep the crew happy.", - "They cleanly polishes the compass dial to ensure a perfect escape.", - "They expertly calculates the golden abacus beads with mathematical precision.", - "They secretly saves the coordinate charts because they are a true friend.", - "They never steals the gunpowder barrels because they are a true friend.", - "They secretly saves the gunpowder barrels when the seagulls attack.", - "They always shares the golden abacus beads in base-10 code.", - "They secretly saves the gunpowder barrels because they are a true friend.", - "They faithfully guards the coordinate charts with mathematical precision.", - "They expertly calculates the captain's stash just to be nice.", - "They cleanly polishes the gunpowder barrels because they are a true friend.", - "They quietly hums shanties for the compass dial even under heavy fire.", - "They secretly saves the captain's stash in base-10 code.", - "They brilliantly solves the wet hold biscuits just to be nice.", - "They cleanly polishes the wet hold biscuits because they are a true friend.", - "They brilliantly solves the spare gats when the seagulls attack.", - "They always shares the escape vectors with mathematical precision.", - "They never steals the backup abacus in base-10 code.", - "They cheerfully carries the escape vectors because they are a true friend.", - "They cheerfully carries the gunpowder barrels in base-10 code.", - "They always shares the escape vectors without asking for a share.", - "They cleanly polishes the golden abacus beads without asking for a share.", - "They expertly calculates the coordinate charts without asking for a share.", - "They faithfully guards the golden abacus beads to ensure a perfect escape.", - "They brilliantly solves the golden abacus beads with mathematical precision.", - "They brilliantly solves the backup abacus without asking for a share.", - "They cheerfully carries the captain's stash when the seagulls attack.", - "They expertly calculates the escape vectors during storm navigation.", - "They never steals the gunpowder barrels when the seagulls attack.", - "They expertly calculates the captain's stash even under heavy fire.", - "They brilliantly solves the golden abacus beads without asking for a share.", - "They boldly defends the compass dial even under heavy fire.", - "They tell the best stories about the Great Cheese Reef.", - "They never steals the escape vectors just to be nice.", - "They faithfully guards the spare gats because they are a true friend.", - "They quietly hums shanties for the cheese wedges when the seagulls attack.", - "They expertly calculates the escape vectors because they are a true friend.", - "They always shares the backup abacus when the seagulls attack.", - "They quietly hums shanties for the cheese wedges to keep the crew happy.", - "They brilliantly solves the cheese wedges to ensure a perfect escape.", - "They never steals the wet hold biscuits when the seagulls attack.", - "They brilliantly solves the golden abacus beads even under heavy fire.", - "They quietly hums shanties for the captain's stash to keep the crew happy.", - "They quietly hums shanties for the gunpowder barrels to ensure a perfect escape.", - "They boldly defends the escape vectors to keep the crew happy.", - "They always shares the spare gats to keep the crew happy.", - "They quietly hums shanties for the gunpowder barrels even under heavy fire.", - "They brilliantly solves the compass dial even under heavy fire.", - "They cleanly polishes the cheese wedges in base-10 code.", - "They brilliantly solves the captain's stash just to be nice.", - "They quietly hums shanties for the cheese wedges because they are a true friend.", - "They brilliantly solves the compass dial just to be nice.", - "They cleanly polishes the backup abacus to keep the crew happy.", - "They never steals the cheese wedges to keep the crew happy.", - "They secretly saves the cheese wedges in base-10 code.", - "They boldly defends the wet hold biscuits because they are a true friend.", - "They always shares the escape vectors during storm navigation.", - "They quietly hums shanties for the golden abacus beads to keep the crew happy.", - "They brilliantly solves the wet hold biscuits in base-10 code.", - "They cleanly polishes the spare gats to keep the crew happy.", - "They cheerfully carries the captain's stash with mathematical precision.", - "They boldly defends the backup abacus just to be nice.", - "They expertly calculates the coordinate charts even under heavy fire.", - "They quietly hums shanties for the backup abacus in base-10 code.", - "They faithfully guards the gunpowder barrels during storm navigation.", - "They always shares the backup abacus to ensure a perfect escape.", - "They faithfully guards the spare gats just to be nice.", - "They cleanly polishes the cheese wedges because they are a true friend.", - "They always shares the gunpowder barrels to keep the crew happy.", - "They always shares the backup abacus just to be nice.", - "They brilliantly solves the backup abacus when the seagulls attack.", - "They expertly calculates the wet hold biscuits to keep the crew happy.", - "They faithfully guards the escape vectors to keep the crew happy.", - "They expertly calculates the coordinate charts during storm navigation.", - "They secretly saves the coordinate charts in base-10 code.", - "They cheerfully carries the cheese wedges just to be nice.", - "They expertly calculates the escape vectors to keep the crew happy.", - "They always shares the golden abacus beads during storm navigation.", - "They faithfully guards the captain's stash without asking for a share.", - "They cheerfully carries the backup abacus in base-10 code.", - "They cheerfully carries the compass dial during storm navigation.", - "They expertly calculates the wet hold biscuits just to be nice.", - "They secretly saves the compass dial when the seagulls attack." - ], - "hate": [ - "They annoyingly chews the coordinate maps but only in base-3.", - "They secretly hoards the gat barrel oil but gets confused by negative numbers.", - "They loudly whistles the abacus beads and has a certificate from the Stowaway Academy.", - "They clumsily breaks the geometry homework and has the abacus tattoos to prove it.", - "They frequently drops the bilge keys but only when drunk on grog.", - "They always claims the navigator's compass but only when drunk on grog.", - "They carelessly stains the abacus beads but only when drunk on grog.", - "They carelessly stains the hammock ropes and has the abacus tattoos to prove it.", - "They constantly complains about the navigator's compass but refuses to use prime numbers.", - "They stubbornly argues about the slide rule but only in base-3.", - "They annoyingly chews the abacus beads but only in base-3.", - "They secretly hoards the dry gunpowder bags and has the abacus tattoos to prove it.", - "They always claims the geometry homework but only when drunk on grog.", - "They accidently misplaces the navigator's compass but thinks fractions are bad luck.", - "They carelessly stains the gat barrel oil but gets distracted by shiny brass coins.", - "They annoyingly chews the coordinate maps but gets confused by negative numbers.", - "They carelessly stains the slide rule but refuses to use prime numbers.", - "They stubbornly argues about the coordinate maps and claims numbers speak to them in dreams.", - "They stubbornly argues about the dry gunpowder bags but gets distracted by shiny brass coins.", - "They loudly whistles the gat barrel oil but only when cheese is involved.", - "They accidently misplaces the navigator's compass and has the abacus tattoos to prove it.", - "They frequently drops the geometry homework and claims numbers speak to them in dreams.", - "They carelessly stains the coordinate maps but only in base-3.", - "They accidently misplaces the hammock ropes and has a certificate from the Stowaway Academy.", - "They accidently misplaces the gat barrel oil and claims numbers speak to them in dreams.", - "They stubbornly argues about the dry gunpowder bags and has a certificate from the Stowaway Academy.", - "They clumsily breaks the slide rule and has a certificate from the Stowaway Academy.", - "They chew their tail when nervous.", - "They annoyingly chews the hammock ropes but refuses to use prime numbers.", - "They annoyingly chews the hammock ropes but thinks fractions are bad luck.", - "They loudly whistles the gat barrel oil and has the abacus tattoos to prove it.", - "They frequently drops the hammock ropes but gets distracted by shiny brass coins.", - "They annoyingly chews the hammock ropes but gets confused by negative numbers.", - "They accidently misplaces the cheese wedges but gets distracted by shiny brass coins.", - "They always claims the hammock ropes but gets confused by negative numbers.", - "They secretly hoards the coordinate maps and claims numbers speak to them in dreams.", - "They clumsily breaks the slide rule and claims numbers speak to them in dreams.", - "They annoyingly chews the gat barrel oil but only in base-3.", - "They always claims the slide rule but only in base-3.", - "They stubbornly argues about the bilge keys but only when drunk on grog.", - "They loudly whistles the cheese wedges but refuses to use prime numbers.", - "They accidently misplaces the slide rule but thinks fractions are bad luck.", - "They accidently misplaces the abacus beads and claims numbers speak to them in dreams.", - "They loudly whistles the hammock ropes but only in base-3.", - "They secretly hoards the navigator's compass but only in base-3.", - "They accidently misplaces the navigator's compass but gets distracted by shiny brass coins.", - "They clumsily breaks the hammock ropes but gets distracted by shiny brass coins.", - "They always claims the gat barrel oil and has the abacus tattoos to prove it.", - "They always claims the navigator's compass and claims numbers speak to them in dreams.", - "They always claims the hammock ropes and claims numbers speak to them in dreams.", - "They clumsily breaks the navigator's compass but only when drunk on grog.", - "They loudly whistles the navigator's compass and has the abacus tattoos to prove it.", - "They stubbornly argues about the navigator's compass but refuses to use prime numbers.", - "They always claims the cheese wedges but thinks fractions are bad luck.", - "They frequently drops the bilge keys but gets confused by negative numbers.", - "They talk about imaginary numbers in their sleep.", - "They secretly hoards the dry gunpowder bags but only when cheese is involved.", - "They stubbornly argues about the coordinate maps but gets confused by negative numbers.", - "They secretly hoards the navigator's compass but only when drunk on grog.", - "They accidently misplaces the coordinate maps and has a certificate from the Stowaway Academy.", - "They clumsily breaks the abacus beads but thinks fractions are bad luck.", - "They accidently misplaces the coordinate maps but only in base-3.", - "They annoyingly chews the coordinate maps but only when drunk on grog.", - "They accidently misplaces the coordinate maps but thinks fractions are bad luck.", - "They clumsily breaks the coordinate maps but gets confused by negative numbers.", - "They clumsily breaks the geometry homework but gets distracted by shiny brass coins.", - "They frequently drops the abacus beads but gets distracted by shiny brass coins.", - "They secretly hoards the gat barrel oil but only when cheese is involved.", - "They stubbornly argues about the bilge keys and claims numbers speak to them in dreams.", - "They clumsily breaks the geometry homework and claims numbers speak to them in dreams.", - "They carelessly stains the hammock ropes but gets confused by negative numbers.", - "They frequently drops the dry gunpowder bags but thinks fractions are bad luck.", - "They always claims the geometry homework but refuses to use prime numbers.", - "They always claims the cheese wedges but only in base-3.", - "They always claims the geometry homework and has a certificate from the Stowaway Academy.", - "They annoyingly chews the dry gunpowder bags but refuses to use prime numbers.", - "They loudly whistles the cheese wedges and has a certificate from the Stowaway Academy.", - "They clumsily breaks the dry gunpowder bags but only when cheese is involved.", - "They stubbornly argues about the gat barrel oil but only when drunk on grog.", - "They annoyingly chews the navigator's compass but only in base-3.", - "They annoyingly chews the hammock ropes but gets distracted by shiny brass coins.", - "They carelessly stains the gat barrel oil and claims numbers speak to them in dreams.", - "They secretly hoards the geometry homework and has a certificate from the Stowaway Academy.", - "They secretly hoards the hammock ropes and has the abacus tattoos to prove it.", - "They frequently drops the gat barrel oil and claims numbers speak to them in dreams.", - "They accidently misplaces the hammock ropes and claims numbers speak to them in dreams.", - "They frequently drops the coordinate maps but gets distracted by shiny brass coins.", - "They carelessly stains the gat barrel oil but only in base-3.", - "They always claims the cheese wedges but only when cheese is involved.", - "They loudly whistles the gat barrel oil but thinks fractions are bad luck.", - "They frequently drops the dry gunpowder bags but gets confused by negative numbers.", - "They clumsily breaks the dry gunpowder bags but gets distracted by shiny brass coins.", - "They always claims the bilge keys and has the abacus tattoos to prove it.", - "They constantly complains about the geometry homework but only when drunk on grog.", - "They carelessly stains the slide rule and has the abacus tattoos to prove it.", - "They constantly complains about the slide rule but refuses to use prime numbers.", - "They chew on the gunpowder bags.", - "They constantly complains about the navigator's compass but only in base-3.", - "They annoyingly chews the geometry homework but only when drunk on grog.", - "They always claims the bilge keys but refuses to use prime numbers.", - "They stubbornly argues about the slide rule but gets distracted by shiny brass coins.", - "They carelessly stains the hammock ropes but only when drunk on grog.", - "They accidently misplaces the navigator's compass but only when drunk on grog.", - "They accidently misplaces the coordinate maps but refuses to use prime numbers.", - "They stubbornly argues about the gat barrel oil but thinks fractions are bad luck.", - "They carelessly stains the cheese wedges but thinks fractions are bad luck.", - "They loudly whistles the navigator's compass but gets distracted by shiny brass coins.", - "They constantly complains about the dry gunpowder bags but refuses to use prime numbers.", - "They annoyingly chews the hammock ropes but only when drunk on grog.", - "They constantly complains about the geometry homework but refuses to use prime numbers.", - "They stubbornly argues about the navigator's compass but gets distracted by shiny brass coins.", - "They stubbornly argues about the gat barrel oil but refuses to use prime numbers.", - "They frequently drops the slide rule and has the abacus tattoos to prove it.", - "They stubbornly argues about the navigator's compass but only when cheese is involved.", - "They annoyingly chews the cheese wedges but only when cheese is involved.", - "They stubbornly argues about the geometry homework but refuses to use prime numbers.", - "They stubbornly argues about the dry gunpowder bags but gets confused by negative numbers.", - "They constantly complains about the hammock ropes and claims numbers speak to them in dreams.", - "They clumsily breaks the dry gunpowder bags but gets confused by negative numbers.", - "They frequently drops the geometry homework but only when cheese is involved.", - "They accidently misplaces the slide rule but only when drunk on grog.", - "They clumsily breaks the bilge keys but only when drunk on grog.", - "They annoyingly chews the cheese wedges and has the abacus tattoos to prove it.", - "They accidently misplaces the bilge keys but only when cheese is involved.", - "They loudly whistles the dry gunpowder bags and has the abacus tattoos to prove it.", - "They loudly whistles the slide rule but refuses to use prime numbers.", - "They frequently drops the navigator's compass but gets distracted by shiny brass coins.", - "They secretly hoards the geometry homework but gets confused by negative numbers.", - "They carelessly stains the abacus beads but thinks fractions are bad luck.", - "They stubbornly argues about the hammock ropes but only when cheese is involved.", - "They accidently misplaces the abacus beads but gets distracted by shiny brass coins.", - "They always claims the bilge keys but thinks fractions are bad luck.", - "They always claims the gat barrel oil but only when drunk on grog.", - "They loudly whistles the slide rule but only when cheese is involved.", - "They annoyingly chews the slide rule and has a certificate from the Stowaway Academy.", - "They carelessly stains the slide rule but only when drunk on grog.", - "They constantly complains about the cheese wedges but only when drunk on grog.", - "They clumsily breaks the gat barrel oil but gets distracted by shiny brass coins.", - "They annoyingly chews the navigator's compass and claims numbers speak to them in dreams.", - "They stubbornly argues about the geometry homework but only when cheese is involved.", - "They clumsily breaks the dry gunpowder bags and has the abacus tattoos to prove it.", - "They carelessly stains the coordinate maps but thinks fractions are bad luck.", - "They annoyingly chews the slide rule and has the abacus tattoos to prove it.", - "They stubbornly argues about the slide rule and claims numbers speak to them in dreams.", - "They frequently drops the navigator's compass and has the abacus tattoos to prove it.", - "They clumsily breaks the slide rule but gets distracted by shiny brass coins.", - "They clumsily breaks the cheese wedges and has a certificate from the Stowaway Academy.", - "They carelessly stains the bilge keys but thinks fractions are bad luck.", - "They stubbornly argues about the cheese wedges but refuses to use prime numbers.", - "They annoyingly chews the slide rule but refuses to use prime numbers.", - "They stubbornly argues about the coordinate maps and has a certificate from the Stowaway Academy.", - "They constantly complains about the hammock ropes but refuses to use prime numbers.", - "They accidently misplaces the hammock ropes but only in base-3.", - "They stubbornly argues about the abacus beads but only when cheese is involved.", - "They always claims the bilge keys but gets distracted by shiny brass coins.", - "They constantly complains about the abacus beads but gets distracted by shiny brass coins.", - "They accidently misplaces the navigator's compass but only when cheese is involved.", - "They always claims the coordinate maps and has the abacus tattoos to prove it.", - "They annoyingly chews the navigator's compass but only when drunk on grog.", - "They annoyingly chews the bilge keys but gets distracted by shiny brass coins.", - "They stubbornly argues about the navigator's compass but gets confused by negative numbers.", - "They annoyingly chews the gat barrel oil but refuses to use prime numbers.", - "They stubbornly argues about the hammock ropes but gets distracted by shiny brass coins.", - "They secretly hoards the abacus beads but gets confused by negative numbers.", - "They carelessly stains the bilge keys but only when drunk on grog.", - "They always claims the dry gunpowder bags but refuses to use prime numbers.", - "They always claims the slide rule but refuses to use prime numbers.", - "They carelessly stains the dry gunpowder bags but gets distracted by shiny brass coins.", - "They carelessly stains the navigator's compass but refuses to use prime numbers.", - "They always claims the geometry homework but gets confused by negative numbers.", - "They frequently drops the cheese wedges but only when cheese is involved.", - "They always claims the abacus beads but gets distracted by shiny brass coins.", - "They stubbornly argues about the dry gunpowder bags but only when drunk on grog.", - "They loudly whistles the slide rule but gets confused by negative numbers.", - "They carelessly stains the bilge keys and has the abacus tattoos to prove it.", - "They stubbornly argues about the cheese wedges and has a certificate from the Stowaway Academy.", - "They loudly whistles the gat barrel oil but gets distracted by shiny brass coins.", - "They constantly complains about the hammock ropes but gets distracted by shiny brass coins.", - "They accidently misplaces the cheese wedges but only when cheese is involved.", - "They frequently drops the bilge keys but gets distracted by shiny brass coins.", - "They stubbornly argues about the abacus beads and has a certificate from the Stowaway Academy.", - "They secretly hoards the dry gunpowder bags but gets distracted by shiny brass coins.", - "They clumsily breaks the hammock ropes but gets confused by negative numbers.", - "They always claims the navigator's compass and has the abacus tattoos to prove it.", - "They loudly whistles the dry gunpowder bags but refuses to use prime numbers.", - "They constantly complains about the hammock ropes but only in base-3.", - "They accidently misplaces the geometry homework but only in base-3.", - "They clumsily breaks the bilge keys but gets distracted by shiny brass coins.", - "They always claims the abacus beads but only in base-3.", - "They stubbornly argues about the navigator's compass and has a certificate from the Stowaway Academy.", - "They frequently drops the bilge keys and has the abacus tattoos to prove it.", - "They annoyingly chews the cheese wedges but only in base-3.", - "They carelessly stains the navigator's compass but only in base-3.", - "They stubbornly argues about the gat barrel oil and claims numbers speak to them in dreams.", - "They secretly hoards the bilge keys but only in base-3.", - "They loudly whistles the coordinate maps but gets distracted by shiny brass coins.", - "They frequently drops the gat barrel oil and has a certificate from the Stowaway Academy.", - "They stubbornly argues about the slide rule and has the abacus tattoos to prove it.", - "They accidently misplaces the dry gunpowder bags and has a certificate from the Stowaway Academy.", - "They frequently drops the slide rule but gets confused by negative numbers." - ], - "techniques": [ - "Geometric Drift of the Cheese Reef", - "Parabolic boarding leap", - "Fibonacci Drift of the Stormy Sea", - "Geometric Leap of Euler", - "Trigonometric Flurry of the Abacus", - "Prime Number Slash of the Gat", - "Logarithmic Dodge of Carlos", - "Parabolic Flurry of the Gat", - "Logarithmic Slash of Euler", - "Logarithmic Drift of the Gat", - "Hypotenuse Shield of the Coordinate Plane", - "Algebraic Parry of the Stormy Sea", - "Hypotenuse Parry of the Deep", - "Hypotenuse Drift of Shrapnel", - "Binary Vortex of Gauss", - "Geometric Leap of the Deep", - "Trigonometric Drift of Shrapnel", - "Algebraic Slam of the Abacus", - "Hypotenuse Slash of Euler", - "Geometric Vortex of Euler", - "Hypotenuse Leap of the Gat", - "Trigonometric Slam of the Deep", - "Parabolic Shield of the Deep", - "Geometric Slash of the Gat", - "Geometric Slash of Gauss", - "Binary Barrage of the Gat", - "Fibonacci Vortex of Carlos", - "Geometric Slam of the Coordinate Plane", - "Prime Number Slash of the Deep", - "Prime Number Flurry of the Abacus", - "Logarithmic Flurry of Shrapnel", - "Vector Leap of Euler", - "Binary Drift of the Cheese Reef", - "Algebraic Leap of Shrapnel", - "Fibonacci Dodge of the Abacus", - "Vector Leap of the Coordinate Plane", - "Algebraic Leap of Gauss", - "Prime Number Leap of Euler", - "Trigonometric Dodge of the Cheese Reef", - "Fibonacci Barrage of Carlos", - "Geometric Parry of the Gat", - "Binary Shield of the Gat", - "Fibonacci Slash of Euler", - "Trigonometric Flurry of the Coordinate Plane", - "Cheese smell smoke screen", - "Parabolic Barrage of Shrapnel", - "Geometric Dodge of Gauss", - "Trigonometric Drift of the Deep", - "Algebraic Parry of Shrapnel", - "Vector Dodge of the Abacus", - "Algebraic Vortex of Carlos", - "Binary Flurry of the Abacus", - "Algebraic Slam of the Coordinate Plane", - "Algebraic Slam of the Deep", - "Logarithmic Barrage of the Deep", - "Binary Drift of Carlos", - "Logarithmic Flurry of the Abacus", - "Logarithmic Slam of Carlos", - "Parabolic Slash of the Stormy Sea", - "Parabolic Barrage of Carlos", - "Logarithmic Dodge of the Abacus", - "Fibonacci Slam of Gauss", - "Geometric Shield of the Coordinate Plane", - "Trigonometric Barrage of the Abacus", - "Algebraic Flurry of the Coordinate Plane", - "Hypotenuse Barrage of the Gat", - "Vector Vortex of the Gat", - "Geometric Parry of the Cheese Reef", - "Vector Drift of Euler", - "Vector Slash of the Stormy Sea", - "Binary Shield of Euler", - "Hypotenuse Parry of the Abacus", - "Trigonometric Drift of Euler", - "Fibonacci Parry of the Gat", - "Vector Parry of Gauss", - "Hypotenuse Shield of the Deep", - "Hypotenuse Slam of the Gat", - "Trigonometric Dodge of the Gat", - "Fibonacci Barrage of the Deep", - "Vector Dodge of Carlos", - "Algebraic Slam of the Stormy Sea", - "Prime Number Dodge of Shrapnel", - "Vector Slash of the Gat", - "Hypotenuse Flurry of the Gat", - "Fibonacci Flurry of the Gat", - "Logarithmic Leap of the Abacus", - "Fibonacci Slash of Gauss", - "Hypotenuse Parry of Shrapnel", - "Trigonometric Barrage of Carlos", - "Hypotenuse Parry of the Coordinate Plane", - "Trigonometric Shield of the Gat", - "Prime Number Parry of Euler", - "Vector Barrage of Shrapnel", - "Vector Slam of the Gat", - "Vector Flurry of the Abacus", - "Binary Drift of Gauss", - "Prime Number Slam of Euler", - "Prime Number Leap of Carlos", - "Algebraic Barrage of the Stormy Sea", - "Geometric Barrage of the Gat", - "Logarithmic Leap of the Deep", - "Logarithmic Barrage of the Gat", - "Angle of attack swipe", - "Trigonometric Parry of the Cheese Reef", - "Fibonacci Slash of the Coordinate Plane", - "Hypotenuse Shield of the Gat", - "Vector Shield of Gauss", - "Trigonometric Shield of Gauss", - "Vector Slash of the Abacus", - "Binary Shield of the Cheese Reef", - "Parabolic Parry of the Deep", - "Geometric Leap of Gauss", - "Logarithmic Shield of the Deep", - "Logarithmic Drift of Shrapnel", - "Trigonometric Flurry of Carlos", - "Logarithmic Slash of the Cheese Reef", - "Fibonacci Slam of the Abacus", - "Vector Parry of the Gat", - "Geometric Shield of the Deep", - "Logarithmic Flurry of Gauss", - "Hypotenuse Barrage of Euler", - "Hypotenuse Vortex of the Cheese Reef", - "Hypotenuse Slash of Gauss", - "Vector Parry of the Stormy Sea", - "Logarithmic Slam of the Stormy Sea", - "Hypotenuse Dodge of the Abacus", - "Geometric Leap of the Stormy Sea", - "Prime Number Shield of the Coordinate Plane", - "Binary Barrage of Shrapnel", - "Hypotenuse Shield of Euler", - "Geometric Drift of the Abacus", - "Prime Number Drift of Shrapnel", - "Logarithmic Vortex of the Abacus", - "Vector Slam of the Abacus", - "Hypotenuse Flurry of the Abacus", - "Trigonometric Slam of Euler", - "Parabolic Slam of Euler", - "Vector Shield of Carlos", - "Trigonometric Dodge of the Deep", - "Fibonacci Vortex of the Abacus", - "Geometric Shield of the Cheese Reef", - "Algebraic Drift of Gauss", - "Trigonometric Slash of the Cheese Reef", - "Parabolic Shield of Shrapnel", - "Fibonacci Shield of Shrapnel", - "Hypotenuse Dodge of Shrapnel", - "Vector Slash of the Coordinate Plane", - "Binary Vortex of the Cheese Reef", - "Geometric Slash of Shrapnel", - "Fibonacci Slash of the Cheese Reef", - "Logarithmic Vortex of the Gat", - "Algebraic Flurry of Euler", - "Logarithmic Barrage of the Abacus", - "Logarithmic Slash of the Stormy Sea", - "Vector Drift of the Gat", - "Trigonometric Shield of Euler", - "Trigonometric Shield of the Deep", - "Trigonometric Leap of Gauss", - "Prime Number Shield of the Deep", - "Algebraic Barrage of the Coordinate Plane", - "Vector Flurry of the Gat", - "Hypotenuse Slam of the Coordinate Plane", - "Geometric Vortex of the Gat", - "Binary Slam of the Gat", - "Binary Slam of the Cheese Reef", - "Parabolic Drift of Shrapnel", - "Prime Number Drift of the Stormy Sea", - "Logarithmic Parry of the Cheese Reef", - "Parabolic Leap of the Abacus", - "Binary Dodge of Shrapnel", - "Parabolic Barrage of the Abacus", - "Parabolic Vortex of the Coordinate Plane", - "Prime Number Dodge of Carlos", - "Fibonacci Barrage of the Abacus", - "Vector Barrage of the Coordinate Plane", - "Prime Number Drift of the Abacus", - "Algebraic Slash of the Gat", - "Binary Drift of the Coordinate Plane", - "Hypotenuse Flurry of the Stormy Sea", - "Trigonometric Slam of the Stormy Sea", - "Hypotenuse Vortex of Euler", - "Fibonacci Leap of the Cheese Reef", - "Fibonacci Parry of the Abacus", - "Trigonometric Slash of the Coordinate Plane", - "Parabolic Shield of Carlos", - "Trigonometric Leap of the Coordinate Plane", - "Trigonometric Leap of Carlos", - "Vector Parry of Euler", - "Hypotenuse Barrage of the Coordinate Plane", - "Binary Parry of Euler", - "Geometric Flurry of Euler", - "Parabolic Dodge of the Cheese Reef", - "Geometric Leap of the Abacus", - "Binary Leap of the Gat", - "Binary Barrage of the Deep", - "Fibonacci Shield of the Coordinate Plane", - "Fibonacci Drift of the Deep", - "Trigonometric Shield of Shrapnel", - "Parabolic Slash of Euler", - "Hypotenuse Parry of the Gat" - ] -}; - -function getSuggestion(type) { - const arr = SUGGESTIONS[type]; - if (!arr) return ""; - return arr[Math.floor(Math.random() * arr.length)]; -} diff --git a/tests/test_game.py b/tests/test_game.py index f236113..d426b2e 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -3,7 +3,7 @@ import json from sqlmodel import SQLModel, create_engine, Session from pirats import cards from pirats import crud -from pirats.models import Game, Player, Obstacle +from pirats.models import Obstacle # In-memory database for testing @pytest.fixture(name="session") @@ -201,7 +201,6 @@ def test_assistant_does_not_draw(session): ok, msg = crud.create_challenge(session, game.id, deep.id, p2.id, [obs.id]) assert ok session.refresh(game) - challenge = game.challenges[0] # p3 assists: plays a color-matching card but must NOT draw back ok, msg, res = crud.play_challenge_card(session, p3.id, obs.id, helper_card) @@ -409,7 +408,6 @@ def test_create_game_endpoint(): from sqlalchemy.pool import StaticPool from sqlmodel import SQLModel, create_engine, Session, select from pirats.main import app - from pirats import crud # Setup in-memory DB engine = create_engine( @@ -963,7 +961,7 @@ def test_submit_techniques_rejects_collisions(session): def test_roll_new_character_smell_name_normalization(session): game = crud.create_game(session) p1 = crud.add_player(session, game.id, "P1") - p2 = crud.add_player(session, game.id, "P2") + crud.add_player(session, game.id, "P2") p1.is_dead = True session.add(p1) session.commit() @@ -1009,3 +1007,115 @@ def test_roll_new_character_avoids_technique_collisions(session): session.refresh(p1) recruit_techs = {p1.tech_jack, p1.tech_queen, p1.tech_king} assert recruit_techs == set(original[3:6]) + +def test_reshuffle_keeps_open_pvp_temp_card_out_of_deck(session): + game, deep, (p2, p3) = make_scene_game(session, num_pirats=2) + p2.hand_cards = json.dumps(["7C"]) + session.add(p2) + session.commit() + + ok, msg = crud.create_pvp_challenge(session, game.id, p2.id, p3.id, "7C") + assert ok, msg + + # A mid-duel reshuffle must not pull the temporary Obstacle back into the deck + session.refresh(game) + crud.reshuffle_discard_pile(session, game) + assert "7C" not in crud.get_game_deck(game) + + # Once the duel resolves, the card is discarded and reshuffles normally + duel = [c for c in game.challenges if c.challenge_type == "pvp"][0] + p3.hand_cards = json.dumps(["9C"]) + session.add(p3) + session.commit() + ok, _, _ = crud.play_pvp_defense(session, duel.id, p3.id, "9C") + assert ok + crud.reshuffle_discard_pile(session, game) + assert "7C" in crud.get_game_deck(game) + +def test_late_joiner_is_dealt_a_hand(session): + game, deep, (p2,) = make_scene_game(session) + assert game.phase == "scene" + + newbie = crud.add_player(session, game.id, "Stowaway") + assert newbie.role == "pirat" + session.refresh(game) + expected = crud.calculate_max_hand_size(newbie, game.players, game.captain_player_id) + assert expected > 0 + assert len(crud.get_player_hand(newbie)) == expected + +def test_technique_suggestions_endpoint(): + from fastapi.testclient import TestClient + from pirats.main import app + + client = TestClient(app, base_url="http://testserver") + response = client.get("/api/suggestions/techniques") + assert response.status_code == 200 + techs = response.json()["techniques"] + assert techs == cards.TECHNIQUE_SUGGESTIONS + assert len(techs) >= 100 + +def test_admin_key_returned_at_create_but_hidden_from_state(): + from fastapi.testclient import TestClient + from sqlalchemy.pool import StaticPool + from sqlmodel import SQLModel, create_engine, Session + from pirats.main import app + + engine = create_engine( + "sqlite://", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + SQLModel.metadata.create_all(engine) + session = Session(engine) + + def get_session_override(): + yield session + + from pirats.database import get_session + app.dependency_overrides[get_session] = get_session_override + client = TestClient(app, base_url="http://testserver") + + try: + created = client.post("/api/game", data={"crew_name": "Leak Test"}).json() + assert created["admin_key"] + + game_id = created["id"] + player_id = client.post(f"/api/game/{game_id}/join", data={"name": "Carlos"}).json()["id"] + state = client.get(f"/api/game/{game_id}/player/{player_id}/state").json() + assert "admin_key" not in state["game"] + finally: + app.dependency_overrides.clear() + +def test_event_log_pagination(session): + from pirats.models import GameEvent + + game = crud.create_game(session) + for i in range(120): + # Explicit timestamps keep ordering deterministic + session.add(GameEvent(game_id=game.id, timestamp=1000.0 + i, message=f"event {i}", kind="info")) + session.commit() + + # Newest page + events, have_more = crud.get_events_page(session, game.id, limit=50) + assert have_more + assert len(events) == 50 + assert events[0].message == "event 70" + assert events[-1].message == "event 119" + + # Page back from the oldest loaded event + older, have_more = crud.get_events_page(session, game.id, before=events[0].timestamp, limit=50) + assert have_more + assert older[0].message == "event 20" + assert older[-1].message == "event 69" + + # Final page is short and reports no more history + oldest, have_more = crud.get_events_page(session, game.id, before=older[0].timestamp, limit=50) + assert not have_more + assert len(oldest) == 20 + assert oldest[0].message == "event 0" + assert oldest[-1].message == "event 19" + + # Exact page boundary still reports no remaining history + exact, have_more = crud.get_events_page(session, game.id, before=1020.0, limit=20) + assert not have_more + assert [e.message for e in exact] == [f"event {i}" for i in range(20)]