BareGit

Refactor codebase to separate logic, components, and orchestration.

- Create game.js: Contains constants, state helpers, and deck management logic.
- Create components.js: Contains pure UI components (Card, WeaponSlot, GameOver).
- Refactor main.js: Reduce to an orchestrator for state and event handling.
- Maintain existing functionality including theme support and state persistence.
Author: MetroWind <chris.corsair@gmail.com>
Date: Sat Jan 3 15:53:06 2026 -0800
Commit: 2c085fe0556b6fdc96e4d65bd813be7daffab0f8

Changes

diff --git a/components.js b/components.js
new file mode 100644
index 0000000..3d99ca9
--- /dev/null
+++ b/components.js
@@ -0,0 +1,89 @@
+import { html } from 'https://unpkg.com/htm/preact/standalone.module.js';
+import { THEMES } from './game.js';
+
+export const Card = ({ card, onClick, is_disabled, onAttack, can_use_weapon, themeKey }) => {
+    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);
+    };
+
+    const theme = THEMES[themeKey];
+    
+    const renderCardContent = () => {
+        if (themeKey === 'Text') {
+            return html`
+                <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>
+            `;
+        } else {
+            const code = `${card.rank}${card.suit_key[0]}`.toLowerCase();
+            const src = `${theme.path}/${code}.${theme.ext}`;
+            return html`<img src="${src}" class="card-image" alt="${card.rank}${card.suit}" />`;
+        }
+    };
+
+    return html`
+        <div class="card ${card.color} ${is_disabled ? 'disabled' : ''}" onClick=${() => !is_disabled && onClick(card)}>
+            ${renderCardContent()}
+            ${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>
+    `;
+};
+
+export const WeaponSlot = ({ weapon, last_enemy_value, themeKey }) => {
+    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} themeKey=${themeKey} /></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>
+    `;
+};
+
+export 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>
+    `;
+};
diff --git a/game.js b/game.js
new file mode 100644
index 0000000..a67ade2
--- /dev/null
+++ b/game.js
@@ -0,0 +1,110 @@
+// --- Constants ---
+export const MAX_HP = 20;
+export const MAX_UNDO_STEPS = 1;
+export const STORAGE_KEY = 'scoundrel_state_v1';
+
+export 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' }
+};
+
+export const RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'];
+
+export const THEMES = {
+    'Text': { label: 'Classic (Text)' },
+    'SimpleSVG': { label: 'Simple SVG', ext: 'svg', path: 'assets/themes/SimpleSVG' }
+};
+
+export 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.",
+    theme: 'Text',
+    history: []
+};
+
+// --- Helpers ---
+export 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);
+};
+
+export 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;
+};
+
+export 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;
+};
+
+export 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));
+    // Limit history to MAX_UNDO_STEPS
+    return [...history, snapshot].slice(-MAX_UNDO_STEPS);
+};
+
+export const saveState = (state) => {
+    try {
+        // Convert Infinity to a placeholder string for JSON serialization
+        const serialized = JSON.stringify(state, (key, value) => {
+            return value === Infinity ? '___Infinity___' : value;
+        });
+        localStorage.setItem(STORAGE_KEY, serialized);
+    } catch (e) {
+        console.error("Failed to save state:", e);
+    }
+};
+
+export const loadState = () => {
+    try {
+        const saved = localStorage.getItem(STORAGE_KEY);
+        if (!saved) return null;
+        return JSON.parse(saved, (key, value) => {
+            return value === '___Infinity___' ? Infinity : value;
+        });
+    } catch (e) {
+        console.error("Failed to load state:", e);
+        return null;
+    }
+};
diff --git a/main.js b/main.js
index ac37b34..8deebb3 100644
--- a/main.js
+++ b/main.js
@@ -1,197 +1,9 @@
 import { h, render, useState, useEffect, html } from 'https://unpkg.com/htm/preact/standalone.module.js';
-
-// --- Constants ---
-const MAX_HP = 20;
-const MAX_UNDO_STEPS = 1;
-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'];
-
-const THEMES = {
-    'Text': { label: 'Classic (Text)' },
-    'SimpleSVG': { label: 'Simple SVG', ext: 'svg', path: 'assets/themes/SimpleSVG' }
-};
-
-// --- 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, themeKey }) => {
-    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);
-    };
-
-    const theme = THEMES[themeKey];
-    
-    const renderCardContent = () => {
-        if (themeKey === 'Text') {
-            return html`
-                <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>
-            `;
-        } else {
-            const code = `${card.rank}${card.suit_key[0]}`.toLowerCase();
-            const src = `${theme.path}/${code}.${theme.ext}`;
-            return html`<img src="${src}" class="card-image" alt="${card.rank}${card.suit}" />`;
-        }
-    };
-
-    return html`
-        <div class="card ${card.color} ${is_disabled ? 'disabled' : ''}" onClick=${() => !is_disabled && onClick(card)}>
-            ${renderCardContent()}
-            ${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, themeKey }) => {
-    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} themeKey=${themeKey} /></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.",
-    theme: 'Text',
-    history: []
-};
-
-const STORAGE_KEY = 'scoundrel_state_v1';
-
-const saveState = (state) => {
-    try {
-        // Convert Infinity to a placeholder string for JSON serialization
-        const serialized = JSON.stringify(state, (key, value) => {
-            return value === Infinity ? '___Infinity___' : value;
-        });
-        localStorage.setItem(STORAGE_KEY, serialized);
-    } catch (e) {
-        console.error("Failed to save state:", e);
-    }
-};
-
-const loadState = () => {
-    try {
-        const saved = localStorage.getItem(STORAGE_KEY);
-        if (!saved) return null;
-        return JSON.parse(saved, (key, value) => {
-            return value === '___Infinity___' ? Infinity : value;
-        });
-    } catch (e) {
-        console.error("Failed to load state:", e);
-        return null;
-    }
-};
+import { 
+    INITIAL_STATE, THEMES, 
+    createDeck, shuffleDeck, saveHistory, saveState, loadState 
+} from './game.js';
+import { Card, WeaponSlot, GameOver } from './components.js';
 
 const App = () => {
     const [state, setState] = useState(() => {
@@ -232,15 +44,6 @@ const App = () => {
         }));
     };
 
-    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));
-        // Limit history to MAX_UNDO_STEPS
-        return [...history, snapshot].slice(-MAX_UNDO_STEPS);
-    };
-
     const undoLastMove = () => {
         if (state.history.length === 0) return;