Changes
diff --git a/design.md b/design.md
new file mode 100644
index 0000000..5c78a46
--- /dev/null
+++ b/design.md
@@ -0,0 +1,146 @@
+# Scoundrel Web Game Design Document
+
+## 1. Overview
+This document outlines the design for a web-based implementation of "Scoundrel," a solo dungeon crawler card game played with a standard poker deck. The application will be a static single-page application (SPA) requiring no server-side code or build steps. It will utilize **Preact** and **HTM** (Hyperscript Tagged Markup) via ES Modules to allow for modern component-based development directly in the browser.
+
+## 2. Game Rules Summary
+The game simulates a dungeon crawl. The goal is to traverse the entire dungeon (the deck) without dying.
+
+### 2.1 Setup
+- **Deck**: Standard 52-card deck.
+- **modifications**: Remove all Red Face cards (J, Q, K of Hearts and Diamonds) and Red Aces.
+- **Start**: Player begins with 20 HP.
+
+### 2.2 Core Loop
+1. **The Room**: A room is formed by dealing 4 cards face up.
+2. **Action**: The player must resolve (interact with) at least 3 of the 4 cards to clear the room.
+3. **Progression**: Once 3 cards are resolved, the remaining 1 card stays, and 3 new cards are dealt to refill the room to 4.
+4. **Fleeing**: The player may choose to flee (skip) a room. The 4 cards are shuffled to the bottom of the deck. You cannot flee two rooms in a row.
+
+### 2.3 Card Types & Interactions
+- **Monsters (Spades ♠ & Clubs ♣)**:
+ - Damage = Card Value (2-10, J=11, Q=12, K=13, A=14).
+ - If unarmed: Take full damage to HP.
+ - If armed: Damage = (Monster Value - Weapon Value). If result is < 0, take 0 damage.
+- **Weapons (Diamonds ♦)**:
+ - Equipping a weapon replaces the current one.
+ - Adds the card's value to attack power.
+ - **Durability**: After a weapon is used to kill a monster, it can only kill monsters *strictly weaker* than the last one killed. (e.g., if you kill a 10, the next kill must be < 10).
+- **Health Potions (Hearts ♥)**:
+ - Heals HP equal to card value (2-10).
+ - Max HP is 20.
+ - You can only use **one** potion per turn (per room refill). Other hearts in the room are useless for healing until the room refreshes (or unless they are discarded to clear the room).
+
+### 2.4 Win/Loss
+- **Win**: The deck is empty, and the final room is cleared.
+- **Lose**: HP drops to 0 or below.
+
+## 3. Technical Architecture
+
+### 3.1 Stack
+- **HTML5**: Semantic structure.
+- **CSS3**: Custom styling, CSS Grid/Flexbox for layout, CSS patterns for card backs.
+- **JavaScript (ES6+)**: Logic and State management.
+- **Libraries**:
+ - `preact`: Lightweight React alternative.
+ - `htm`: JSX-like syntax using tagged templates, eliminating the need for Babel/transpilation.
+ - *Import strategy*: ES Modules via `unpkg` or `esm.sh`.
+
+### 3.2 File Structure
+```text
+/
+├── index.html # Entry point, imports main.js
+├── style.css # All visual styles
+└── main.js # Game logic, state management, and UI components
+```
+
+### 3.3 Coding Conventions
+- **Functions**: `camelCase` (e.g., `calculateDamage`)
+- **Variables**: `snake_case` (e.g., `current_hp`)
+- **Global Constants**: `UPPER_CASE` (e.g., `MAX_HP`)
+- **Comments**: JSDoc style for complex logic.
+
+## 4. Data Structures
+
+### 4.1 Card Representation
+```javascript
+const CARD_SUITS = {
+ HEARTS: '♥',
+ DIAMONDS: '♦',
+ CLUBS: '♣',
+ SPADES: '♠'
+};
+
+// Card Object
+{
+ suit: '♥', // One of CARD_SUITS
+ rank: 'K', // Display rank (2-10, J, Q, K, A)
+ value: 13, // Numerical value for logic
+ id: 'unique_id_string', // For React keys
+ type: 'POTION' // POTION, WEAPON, or MONSTER
+}
+```
+
+### 4.2 Game State
+```javascript
+{
+ current_hp: 20,
+ max_hp: 20,
+ deck: [], // Array of Card objects
+ room: [], // Array of Card objects (max 4)
+ discard_pile: [], // Array of Card objects (cleared cards)
+
+ // Weapon State
+ equipped_weapon: null, // Card object or null
+ last_enemy_value: null, // For tracking weapon durability rule
+
+ // Turn State
+ cards_played_this_turn: 0, // Need 3 to advance
+ potion_used_this_turn: false,
+ can_flee: true,
+
+ // History
+ defeated_monsters: [] // Array of Card objects (visual appeal)
+}
+```
+
+## 5. UI Design
+
+### 5.1 Layout
+The screen will be divided into three main sections:
+1. **Header/Stats**: Title, Current HP bar, Deck count.
+2. **The Dungeon (Center)**:
+ - **Room Area**: The 4 active cards laid out horizontally.
+ - **Weapon Slot**: Shows current weapon and its "max killable value".
+ - **Controls**: "Run Away" button (if applicable).
+3. **History/Footer**:
+ - Pile of defeated monsters (stacked visually).
+ - Rules summary / Help toggle.
+
+### 5.2 Card Visuals
+- **Face**: White background with red (Hearts/Diamonds) or black (Clubs/Spades) text. Large unicode suit symbol in center. Corners show Rank + Suit.
+- **Back**: CSS `repeating-linear-gradient` to create a cross-hatch or diagonal pattern.
+
+## 6. Implementation Logic
+
+### 6.1 Initialization (`initGame`)
+1. Generate full deck.
+2. Filter out Red Faces (J,Q,K) and Red Aces.
+3. Shuffle.
+4. Deal 4 cards to `room`.
+
+### 6.2 Interactions
+- **Clicking a Card**:
+ - *If Monster*: Calculate damage. `current_hp -= max(0, monster.value - (weapon ? weapon.value : 0))`. If weapon used, update `last_enemy_value`. Check validity (weapon durability). Move monster to `defeated_monsters`.
+ - *If Weapon*: Move current weapon (if any) to discard. Set new weapon. Reset `last_enemy_value` to infinity (or max).
+ - *If Potion*: If `!potion_used_this_turn`, `current_hp = min(20, current_hp + potion.value)`. Mark potion used. Move card to discard.
+- **State Update**: Increment `cards_played_this_turn`.
+- **Turn End**: If `cards_played_this_turn == 3`:
+ - Deal 3 new cards.
+ - Reset `cards_played_this_turn = 0`.
+ - Reset `potion_used_this_turn = false`.
+ - Enable `can_flee`.
+
+## 7. Resources
+- [Rules Description](https://rpdillon.net/scoundrel.html)
+- [Official PDF](http://www.stfj.net/art/2011/Scoundrel.pdf)
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..602c403
--- /dev/null
+++ b/index.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Scoundrel</title>
+ <link rel="stylesheet" href="style.css">
+ <style>
+ #error-log {
+ display: none;
+ position: fixed;
+ top: 0; left: 0; right: 0;
+ background: #e74c3c;
+ color: white;
+ padding: 20px;
+ z-index: 1000;
+ white-space: pre-wrap;
+ font-family: monospace;
+ }
+ </style>
+</head>
+<body>
+ <div id="error-log"></div>
+ <div id="app"></div>
+
+ <script>
+ window.onerror = function(message, source, lineno, colno, error) {
+ const errorLog = document.getElementById('error-log');
+ errorLog.style.display = 'block';
+ errorLog.textContent += `Error: ${message}\nAt: ${source}:${lineno}:${colno}\n\n`;
+ };
+ </script>
+
+ <!-- Main Game Script -->
+ <script type="module" src="./main.js"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/main.js b/main.js
new file mode 100644
index 0000000..c57029a
--- /dev/null
+++ b/main.js
@@ -0,0 +1,336 @@
+import { h, render, useState, useEffect, html } from 'https://unpkg.com/htm/preact/standalone.module.js';
+
+// --- Constants ---
+const MAX_HP = 20;
+const SUITS = {
+ HEARTS: { symbol: '♥', color: 'red', type: 'POTION' },
+ DIAMONDS: { symbol: '♦', color: 'red', type: 'WEAPON' },
+ CLUBS: { symbol: '♣', color: 'black', type: 'MONSTER' },
+ SPADES: { symbol: '♠', color: 'black', type: 'MONSTER' }
+};
+
+const RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'];
+
+// --- Helpers ---
+const getCardValue = (rank) => {
+ if (rank === 'A') return 14;
+ if (rank === 'K') return 13;
+ if (rank === 'Q') return 12;
+ if (rank === 'J') return 11;
+ return parseInt(rank, 10);
+};
+
+const createDeck = () => {
+ let deck = [];
+ let id_counter = 0;
+ for (const [suit_key, suit_data] of Object.entries(SUITS)) {
+ for (const rank of RANKS) {
+ if ((suit_key === 'HEARTS' || suit_key === 'DIAMONDS') && ['J', 'Q', 'K', 'A'].includes(rank)) continue;
+ deck.push({
+ id: `card-${id_counter++}`,
+ suit: suit_data.symbol,
+ color: suit_data.color,
+ type: suit_data.type,
+ rank: rank,
+ value: getCardValue(rank),
+ suit_key: suit_key
+ });
+ }
+ }
+ return deck;
+};
+
+const shuffleDeck = (deck) => {
+ const new_deck = [...deck];
+ for (let i = new_deck.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [new_deck[i], new_deck[j]] = [new_deck[j], new_deck[i]];
+ }
+ return new_deck;
+};
+
+// --- Components ---
+
+const Card = ({ card, onClick, is_disabled, onAttack, can_use_weapon }) => {
+ if (!card) return html`<div class="card-slot-empty"></div>`;
+
+ const handleWeaponClick = (e) => {
+ e.stopPropagation();
+ onAttack(card, true);
+ };
+
+ const handleBareHandClick = (e) => {
+ e.stopPropagation();
+ onAttack(card, false);
+ };
+
+ return html`
+ <div class="card ${card.color} ${is_disabled ? 'disabled' : ''}" onClick=${() => !is_disabled && onClick(card)}>
+ <div class="card-corner top-left">
+ <span>${card.rank}</span>
+ <span>${card.suit}</span>
+ </div>
+ <div class="card-center">
+ ${card.suit}
+ </div>
+ <div class="card-corner bottom-right">
+ <span>${card.rank}</span>
+ <span>${card.suit}</span>
+ </div>
+ ${card.type === 'MONSTER' && !is_disabled ? html`
+ <div class="card-overlay">
+ <button class="overlay-btn" title="Attack with Weapon" disabled=${!can_use_weapon} onClick=${handleWeaponClick}>
+ 🗡️
+ </button>
+ <button class="overlay-btn" title="Bare-handed" onClick=${handleBareHandClick}>
+ 👊
+ </button>
+ </div>
+ ` : ''}
+ </div>
+ `;
+};
+
+const WeaponSlot = ({ weapon, last_enemy_value }) => {
+ if (!weapon) {
+ return html`
+ <div class="weapon-slot"><span>No Weapon</span></div>
+ <div class="weapon-info">
+ <p><strong>Unarmed</strong></p>
+ <p>Attack Power: 0</p>
+ </div>
+ `;
+ }
+
+ const max_kill_msg = last_enemy_value === Infinity ? "Any" : `< ${last_enemy_value}`;
+ return html`
+ <div class="weapon-slot"><${Card} card=${weapon} is_disabled=${true} /></div>
+ <div class="weapon-info">
+ <p><strong>Equipped: ${weapon.rank}${weapon.suit}</strong></p>
+ <p>Power: ${weapon.value} | Max Kill: ${max_kill_msg}</p>
+ </div>
+ `;
+};
+
+const GameOver = ({ won, hp, reset_game }) => {
+ const title = won ? "VICTORY!" : "YOU DIED";
+ const css_class = won ? "win" : "lose";
+ return html`
+ <div class="game-over ${css_class}">
+ <h2>${title}</h2>
+ <p>Final Health: ${won ? hp : "DEAD"}</p>
+ <button onClick=${reset_game}>Play Again</button>
+ </div>
+ `;
+};
+
+// --- Main App ---
+
+const INITIAL_STATE = {
+ hp: 20,
+ max_hp: 20,
+ deck: [],
+ room: [null, null, null, null], // Fixed indices
+ discard_pile: [],
+ defeated_monsters: [],
+ weapon: null,
+ last_enemy_value: Infinity,
+ cards_played_this_turn: 0,
+ potion_used_this_turn: false,
+ can_flee: true,
+ game_over: false,
+ won: false,
+ fled_last_turn: false,
+ msg: "Welcome to the dungeon.",
+ history: []
+};
+
+const App = () => {
+ const [state, setState] = useState(INITIAL_STATE);
+
+ useEffect(() => { startGame(); }, []);
+
+ const startGame = () => {
+ const full_deck = shuffleDeck(createDeck());
+ const room = full_deck.splice(0, 4);
+ setState({ ...INITIAL_STATE, deck: full_deck, room, msg: "A new dungeon awaits." });
+ };
+
+ const saveHistory = (currentState) => {
+ // Deep copy state excluding history itself to avoid recursion/heavy memory usage
+ const { history, ...stateToSave } = currentState;
+ // Basic deep copy for this simple state structure (arrays/objects)
+ const snapshot = JSON.parse(JSON.stringify(stateToSave));
+ return [...history, snapshot];
+ };
+
+ const undoLastMove = () => {
+ if (state.history.length === 0) return;
+
+ const previousState = state.history[state.history.length - 1];
+ const remainingHistory = state.history.slice(0, -1);
+
+ // Fix JSON.stringify(Infinity) -> null issue
+ if (previousState.last_enemy_value === null) {
+ previousState.last_enemy_value = Infinity;
+ }
+
+ setState({
+ ...previousState,
+ history: remainingHistory,
+ msg: "Undid last move."
+ });
+ };
+
+ const resolveCard = (card, use_weapon = true) => {
+ let new_state = {
+ ...state,
+ history: saveHistory(state)
+ };
+ let card_resolved = false;
+
+ if (card.type === 'MONSTER') {
+ let damage_taken = 0;
+ if (use_weapon && new_state.weapon && card.value < new_state.last_enemy_value) {
+ damage_taken = Math.max(0, card.value - new_state.weapon.value);
+ new_state.last_enemy_value = card.value;
+ new_state.msg = `Slayed ${card.rank}${card.suit} with weapon. Took ${damage_taken} damage.`;
+ new_state.defeated_monsters = [...new_state.defeated_monsters, card];
+ } else {
+ damage_taken = card.value;
+ new_state.msg = `Fought ${card.rank}${card.suit} bare-handed. Took ${damage_taken} damage.`;
+ new_state.discard_pile = [...new_state.discard_pile, card];
+ }
+ new_state.hp -= damage_taken;
+ card_resolved = true;
+ } else if (card.type === 'WEAPON') {
+ if (new_state.weapon) {
+ new_state.discard_pile = [
+ ...new_state.discard_pile,
+ new_state.weapon,
+ ...new_state.defeated_monsters
+ ];
+ }
+ new_state.defeated_monsters = [];
+ new_state.weapon = card;
+ new_state.last_enemy_value = Infinity;
+ new_state.msg = `Equipped ${card.rank}${card.suit}.`;
+ card_resolved = true;
+ } else if (card.type === 'POTION') {
+ if (!new_state.potion_used_this_turn) {
+ const old_hp = new_state.hp;
+ new_state.hp = Math.min(new_state.max_hp, new_state.hp + card.value);
+ new_state.potion_used_this_turn = true;
+ new_state.msg = `Healed ${new_state.hp - old_hp} HP.`;
+ } else {
+ new_state.msg = "Potion wasted!";
+ }
+ new_state.discard_pile = [...new_state.discard_pile, card];
+ card_resolved = true;
+ }
+
+ if (card_resolved) {
+ new_state.room = new_state.room.map(c => c && c.id === card.id ? null : c);
+ new_state.cards_played_this_turn += 1;
+ if (new_state.hp <= 0) { new_state.game_over = true; new_state.won = false; }
+ else if (new_state.room.filter(c => c !== null).length === 1 && new_state.deck.length > 0) {
+ const remaining_card = new_state.room.find(c => c !== null);
+ const next_cards = new_state.deck.splice(0, 3);
+ new_state.room = [remaining_card, ...next_cards];
+ new_state.cards_played_this_turn = 0;
+ new_state.potion_used_this_turn = false;
+ new_state.fled_last_turn = false;
+ } else if (new_state.room.every(c => c === null) && new_state.deck.length === 0) {
+ new_state.game_over = true; new_state.won = true;
+ }
+ setState(new_state);
+ }
+ };
+
+ const handleCardClick = (card) => {
+ if (state.game_over) return;
+ if (card.type === 'MONSTER') {
+ // Monsters are handled via overlay buttons
+ return;
+ } else {
+ resolveCard(card);
+ }
+ };
+
+ const fleeRoom = () => {
+ const active_cards = state.room.filter(c => c !== null);
+ const new_deck = [...state.deck, ...active_cards];
+ const new_room = new_deck.splice(0, 4);
+ setState({
+ ...state,
+ deck: new_deck,
+ room: new_room,
+ fled_last_turn: true,
+ cards_played_this_turn: 0,
+ potion_used_this_turn: false,
+ msg: "Fled the room.",
+ history: saveHistory(state)
+ });
+ };
+
+ const can_flee_check = !state.fled_last_turn && state.cards_played_this_turn === 0 && state.deck.length > 0;
+
+ return html`
+ <header>
+ <h1>SCOUNDREL</h1>
+ <div class="stats-bar">
+ <span class="hp-bar">HP: ${state.hp} / ${state.max_hp}</span>
+ <span class="deck-count">Deck: ${state.deck.length}</span>
+ </div>
+ </header>
+
+ <div class="game-board">
+ <div class="weapon-area">
+ <div style="display: flex; gap: 20px; align-items: center;">
+ <${WeaponSlot} weapon=${state.weapon} last_enemy_value=${state.last_enemy_value} />
+ </div>
+ <div class="defeated-container">
+ <strong>Defeated:</strong>
+ <div class="defeated-pile">
+ ${state.defeated_monsters.map(m => html`<div class="mini-card ${m.color}" title="${m.rank}${m.suit}">${m.rank}${m.suit}</div>`)}
+ </div>
+ </div>
+ </div>
+
+ <div style="text-align: center; height: 1.2em; font-style: italic;">${state.msg}</div>
+
+ <div class="room-container">
+ <div class="room-cards">
+ ${state.room.map((card, idx) => html`<${Card}
+ key=${card ? card.id : 'empty-'+idx}
+ card=${card}
+ onClick=${handleCardClick}
+ onAttack=${resolveCard}
+ can_use_weapon=${card && state.weapon && card.value < state.last_enemy_value}
+ />`)}
+ </div>
+ </div>
+
+ <div class="actions">
+ <button onClick=${undoLastMove} disabled=${state.history.length === 0}>Undo</button>
+ <button onClick=${fleeRoom} disabled=${!can_flee_check}>Run Away</button>
+ </div>
+ </div>
+
+ <div class="history-area">
+ <div class="discard-container">
+ <strong>Discard Pile (Newest First):</strong>
+ <div class="discard-pile">
+ ${state.discard_pile.slice().reverse().map(c => html`<div class="mini-card ${c.color}" title="${c.rank}${c.suit}">${c.rank}${c.suit}</div>`)}
+ </div>
+ </div>
+ <div class="rules-container">
+ <p><strong>Rules:</strong> Standard 52-card deck (no red face cards/aces). Play 3 of 4 cards to advance. Hearts heal (1/turn). Diamonds are weapons. Weapons durability: next monster must be weaker than last. <a href="https://rpdillon.net/scoundrel.html" target="_blank">Full Rules</a></p>
+ </div>
+ </div>
+
+ ${state.game_over && html`<${GameOver} won=${state.won} hp=${state.hp} reset_game=${startGame} />`}
+ `;
+};
+
+render(html`<${App} />`, document.getElementById('app'));
\ No newline at end of file
diff --git a/prompts.md b/prompts.md
new file mode 100644
index 0000000..44c01cf
--- /dev/null
+++ b/prompts.md
@@ -0,0 +1,47 @@
+Create a design doc to `design.md` which describe an implement of the
+Scoundrel poker game as a web game. This is a solo dungeon crawler
+played with a standard deck of poker cards.
+
+The rules are documented at the following places:
+
+* https://rpdillon.net/scoundrel.html.
+* http://www.stfj.net/art/2011/Scoundrel.pdf
+
+Your goal is to create a game that can be hosted statically in a HTTP
+server. It should be purely run in browser. There will be not
+server-side code. For javascript libraries, use Preact if you think
+it’s appropriate. The code should be able to run as-is in browser
+without any building steps. In terms of coding style, functions should
+be in `camelCase`, and variables should be in `snake_case`. Global
+constants should be `UPPER_CASE`. Use text and unicode symbols for the
+card faces, and some kind of CSS pattern for the card back.
+
+In principle, the only cards the player is required to see are the 4
+cards that form the room. However, for visual appeal, the game should
+also show the current weapon card and all the monster cards it
+defeated.
+
+The game should contain a short description of the game rules, and
+links to the external documents I linked above.
+
+The design doc should include a section that describes the rule of the
+game.
+
+You should only create the design doc for now.
+
+----
+
+This project is web implementation of the Scoundrel poker game. See
+`design.md` for the overall design.
+
+Right now when the player pick a monster card, they have to then click
+a button to either attack using weapon or attack bare-handed. Change
+this so that when the player hover the mouse to the monster card,
+there will be an overlay on the card with two buttons: attach with
+weapon or attach bare-handed. Both buttons should be square and with
+an emoji on it.
+
+----
+
+
+TODO: show list of discarded cards
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..9f252c4
--- /dev/null
+++ b/style.css
@@ -0,0 +1,323 @@
+:root {
+ --bg-color: #2c3e50;
+ --text-color: #ecf0f1;
+ --card-width: 120px;
+ --card-height: 168px; /* 1.4 aspect ratio */
+ --card-radius: 10px;
+ --accent-red: #e74c3c;
+ --accent-black: #2c3e50;
+ --highlight: #f1c40f;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ background-color: var(--bg-color);
+ color: var(--text-color);
+ margin: 0;
+ padding: 0;
+ display: flex;
+ justify-content: center;
+ min-height: 100vh;
+}
+
+#app {
+ width: 100%;
+ max-width: 800px;
+ padding: 20px;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+/* Header & Stats */
+header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 2px solid rgba(255,255,255,0.1);
+ padding-bottom: 10px;
+}
+
+h1 { margin: 0; font-size: 1.5rem; }
+
+.stats-bar {
+ display: flex;
+ gap: 20px;
+ font-size: 1.2rem;
+ font-weight: bold;
+}
+
+.hp-bar { color: var(--accent-red); }
+.deck-count { color: var(--highlight); }
+
+/* Game Area */
+.game-board {
+ display: flex;
+ flex-direction: column;
+ gap: 40px;
+ flex-grow: 1;
+}
+
+/* Weapon Slot */
+.weapon-area {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+ background: rgba(0,0,0,0.2);
+ padding: 15px;
+ border-radius: var(--card-radius);
+}
+
+.weapon-slot {
+ width: var(--card-width);
+ height: var(--card-height);
+ border: 2px dashed rgba(255,255,255,0.3);
+ border-radius: var(--card-radius);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.9rem;
+ color: rgba(255,255,255,0.5);
+ position: relative;
+}
+
+.weapon-info {
+ flex-grow: 1;
+}
+
+/* Room (The 4 cards) */
+.room-container {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ min-height: calc(var(--card-height) + 20px);
+}
+
+.room-cards {
+ display: grid;
+ grid-template-columns: repeat(4, var(--card-width));
+ gap: 15px;
+ justify-content: center;
+}
+
+/* Actions */
+.actions {
+ display: flex;
+ justify-content: center;
+ gap: 10px;
+}
+
+.card-slot-empty {
+ width: var(--card-width);
+ height: var(--card-height);
+ border: 2px dashed rgba(255,255,255,0.1);
+ border-radius: var(--card-radius);
+}
+
+/* History / Footer */
+.history-area {
+ margin-top: auto;
+ border-top: 2px solid rgba(255,255,255,0.1);
+ padding-top: 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.defeated-container, .discard-container {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+}
+
+.defeated-pile, .discard-pile {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 3px;
+ max-height: 120px;
+ overflow-y: auto;
+ background: rgba(0,0,0,0.1);
+ padding: 5px;
+ border-radius: 5px;
+}
+
+.mini-card {
+ width: 30px;
+ height: 42px;
+ background: white;
+ border-radius: 3px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1rem;
+ color: black;
+ border: 1px solid #ccc;
+}
+
+.mini-card.red { color: var(--accent-red); }
+.mini-card.black { color: var(--accent-black); }
+
+
+/* Card Styles */
+.card {
+ width: var(--card-width);
+ height: var(--card-height);
+ background-color: white;
+ border-radius: var(--card-radius);
+ position: relative;
+ box-shadow: 0 4px 6px rgba(0,0,0,0.3);
+ cursor: pointer;
+ transition: transform 0.2s, box-shadow 0.2s;
+ user-select: none;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 8px 12px rgba(0,0,0,0.4);
+}
+
+.card.red { color: var(--accent-red); }
+.card.black { color: var(--accent-black); }
+
+.card-corner {
+ position: absolute;
+ font-size: 1.2rem;
+ font-weight: bold;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ line-height: 1;
+}
+
+.top-left { top: 5px; left: 5px; }
+.bottom-right { bottom: 5px; right: 5px; transform: rotate(180deg); }
+
+.card-center {
+ font-size: 3.5rem;
+}
+
+/* Card Back Pattern */
+.card-back {
+ width: 100%;
+ height: 100%;
+ border-radius: var(--card-radius);
+ background-color: #34495e;
+ background-image:
+ repeating-linear-gradient(
+ 45deg,
+ #2c3e50,
+ #2c3e50 10px,
+ #34495e 10px,
+ #34495e 20px
+ );
+ border: 4px solid white;
+ box-sizing: border-box;
+}
+
+/* Overlay for Monster Cards */
+.card-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(44, 62, 80, 0.9);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ opacity: 0;
+ transition: opacity 0.2s;
+ border-radius: var(--card-radius);
+ pointer-events: none; /* Let clicks pass through if hidden */
+ z-index: 10;
+}
+
+.card:hover .card-overlay {
+ opacity: 1;
+ pointer-events: auto;
+}
+
+.overlay-btn {
+ width: 40px;
+ height: 40px;
+ font-size: 1.5rem;
+ cursor: pointer;
+ border: none;
+ border-radius: 5px;
+ background: #ecf0f1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: transform 0.1s, background-color 0.1s;
+ padding: 0;
+}
+
+.overlay-btn:hover {
+ transform: scale(1.1);
+ background: white;
+}
+
+.overlay-btn:active {
+ transform: scale(0.95);
+}
+
+.overlay-btn:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+ filter: grayscale(1);
+ transform: none;
+}
+
+/* Modal / Rules */
+.rules-container {
+ background: rgba(0,0,0,0.3);
+ padding: 15px;
+ border-radius: 5px;
+ font-size: 0.9rem;
+ line-height: 1.4;
+}
+
+.rules-container a { color: var(--highlight); }
+
+.game-over {
+ position: fixed;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background: rgba(0,0,0,0.85);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ z-index: 100;
+ text-align: center;
+}
+
+.game-over h2 { font-size: 3rem; margin-bottom: 20px; }
+.game-over.win { color: var(--highlight); }
+.game-over.lose { color: var(--accent-red); }
+
+/* Utility */
+.shake {
+ animation: shake 0.5s;
+}
+
+@keyframes shake {
+ 0% { transform: translate(1px, 1px) rotate(0deg); }
+ 10% { transform: translate(-1px, -2px) rotate(-1deg); }
+ 20% { transform: translate(-3px, 0px) rotate(1deg); }
+ 30% { transform: translate(3px, 2px) rotate(0deg); }
+ 40% { transform: translate(1px, -1px) rotate(1deg); }
+ 50% { transform: translate(-1px, 2px) rotate(-1deg); }
+ 60% { transform: translate(-3px, 1px) rotate(0deg); }
+ 70% { transform: translate(3px, 1px) rotate(-1deg); }
+ 80% { transform: translate(-1px, -1px) rotate(1deg); }
+ 90% { transform: translate(1px, 2px) rotate(0deg); }
+ 100% { transform: translate(1px, -2px) rotate(-1deg); }
+}
\ No newline at end of file