diff --git a/src/main.js b/src/main.js index 239906f..9874ac4 100644 --- a/src/main.js +++ b/src/main.js @@ -113,7 +113,7 @@ const root = { }, async onWin() { - this.log("Enemy defeated! šŸŽ‰"); + this.log("Enemy defeated!"); const goldReward = Math.floor(Math.random() * 20) + 15; // 15-35 gold this.player.gold = (this.player.gold || 0) + goldReward; @@ -134,7 +134,7 @@ const root = { this.map = MAPS[nextAct]; this.nodeId = this.map.nodes.find(n => n.kind === "start").id; this.completedNodes = []; - this.log(`šŸŽ‰ Act ${this.currentAct === "act2" ? "II" : "I"} Complete! Advancing to the next challenge...`); + this.log(`Act ${this.currentAct === "act2" ? "II" : "I"} Complete! Advancing to the next challenge...`); // Save Act 2 checkpoint when first reaching it if (nextAct === "act2") { @@ -218,7 +218,7 @@ const root = { timestamp: Date.now() }; localStorage.setItem('birthday-spire-act2-checkpoint', JSON.stringify(checkpointData)); - this.log("šŸŽÆ Act 2 checkpoint saved!"); + this.log("Act 2 checkpoint saved!"); } catch (e) { console.warn('Failed to save Act 2 checkpoint:', e); } @@ -240,7 +240,7 @@ const root = { this.relicStates = data.relicStates || []; this._battleInProgress = false; - this.log("šŸŽÆ Restarting from Act 2 checkpoint..."); + this.log("Restarting from Act 2 checkpoint..."); return true; } return false; diff --git a/src/ui/render.js b/src/ui/render.js index ce8d7f3..7512c1c 100644 --- a/src/ui/render.js +++ b/src/ui/render.js @@ -743,7 +743,7 @@ export function renderUpgrade(root) { root.app.innerHTML = `
-

ā¬†ļø Upgrade a Card

+

Upgrade a Card

Select a card from your deck to permanently improve it

diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index 55b95c5..0000000 --- a/tests/README.md +++ /dev/null @@ -1,162 +0,0 @@ -# Birthday Spire Test Suite - -This directory contains comprehensive tests for all card mechanics in Birthday Spire. - -## Files - -- **`card-tests.js`** - Complete test suite with all card tests -- **`run-tests.js`** - Node.js command-line test runner -- **`test-runner.html`** - Browser-based test runner with UI -- **`README.md`** - This documentation - -## Running Tests - -### Command Line (Node.js) - -```bash -# Run all tests -npm test - -# Run tests with verbose output -npm run test:verbose - -# Check test coverage -npm run test:coverage - -# Test a specific card -npm run test:card strike - -# Or directly with node -node tests/run-tests.js -node tests/run-tests.js --verbose -node tests/run-tests.js --coverage -node tests/run-tests.js --card=strike -``` - -### Browser - -1. Start the development server: - ```bash - npm run serve - ``` - -2. Open `http://localhost:8002/tests/test-runner.html` in your browser - -3. Use the interactive test runner: - - **Run All Tests** - Executes sample test suite - - **Check Coverage** - Shows test coverage statistics - - **Test Specific Card** - Test any card by ID - - **Clear Output** - Clears the test output - -## Test Structure - -Each test follows this pattern: - -```javascript -testCard('cardId', setupFn, assertionFn) -``` - -- **`cardId`** - The card to test -- **`setupFn(ctx)`** - Optional setup function to modify test context -- **`assertionFn(result, ctx)`** - Function to verify the test results - -### Example Test - -```javascript -strike: () => testCard('strike', null, (result) => { - if (result.finalState.enemyHp !== 94) { - throw new Error(`Expected enemy HP 94, got ${result.finalState.enemyHp}`); - } -}) -``` - -## Test Context - -The test context provides a mock battle environment: - -```javascript -{ - player: { hp, maxHp, energy, block, hand, deck, draw, discard, ... }, - enemy: { hp, maxHp, block, weak, vuln, intent, ... }, - flags: { skipThisTurn, nextCardFree, doubleNextCard, ... }, - logs: [], // Captured log messages - - // Available functions: - deal(target, amount), - draw(n), - applyWeak(target, amount), - applyVulnerable(target, amount), - intentIsAttack(), - scalarFromWeak(base), - forceEndTurn(), - promptExhaust(count), - moveFromDiscardToHand(cardId), - countCardType(type), - replayCard(card) -} -``` - -## Adding New Tests - -To add a test for a new card: - -1. Add the test to the `tests` object in `run-tests.js`: - -```javascript -my_new_card: () => testCard('my_new_card', - (ctx) => { - // Setup test conditions - ctx.player.hp = 30; - ctx.enemy.block = 5; - }, - (result, ctx) => { - // Assert expected results - if (result.finalState.enemyHp !== expectedHp) { - throw new Error(`Expected enemy HP ${expectedHp}, got ${result.finalState.enemyHp}`); - } - } -) -``` - -## Test Coverage - -Current test coverage includes: - -- āœ… Basic attack/skill cards (strike, defend, etc.) -- āœ… Energy manipulation cards (coffee_rush, etc.) -- āœ… Complex effect cards (macro, segfault, etc.) -- āœ… Flag-based cards (just_one_game, pair_programming, etc.) -- āœ… Advanced mechanics (ctrl_z, npm_audit, infinite_loop, etc.) -- āœ… Healing cards (stack_trace, refactor, etc.) -- āœ… Multi-hit cards (merge_conflict, etc.) -- āœ… Self-damage cards (production_deploy, etc.) - -Run `npm run test:coverage` to see detailed coverage statistics. - -## Debugging Tests - -For debugging failed tests: - -1. Use `--verbose` flag to see detailed logs -2. Test specific cards with `--card=cardId` -3. Check the browser test runner for interactive debugging -4. Examine the test context setup and assertions - -## Integration with CI/CD - -The test suite returns appropriate exit codes: -- `0` - All tests passed -- `1` - One or more tests failed - -This makes it suitable for continuous integration systems. - -## Extending Tests - -To test more complex scenarios: - -- **Multi-card combos**: Set up multiple cards in hand/play sequence -- **Relic interactions**: Add relic states to context -- **Status effect combinations**: Test weak/vulnerable interactions -- **Edge cases**: Test with 0 energy, full hand, empty deck, etc. - -The test framework is designed to be flexible and extensible for any card mechanics you add to the game. diff --git a/tests/card-tests.js b/tests/card-tests.js deleted file mode 100644 index 6d2ba2d..0000000 --- a/tests/card-tests.js +++ /dev/null @@ -1,430 +0,0 @@ -/** - * Comprehensive Card Test Suite for Birthday Spire - * Tests all card mechanics and effects to ensure they work correctly - */ - -import { CARDS, STARTER_DECK, CARD_POOL } from '../src/data/cards.js'; -import { ENEMIES } from '../src/data/enemies.js'; -import { makePlayer } from '../src/engine/core.js'; -import { makeBattleContext, createBattle } from '../src/engine/battle.js'; -import { cloneCard } from '../src/engine/core.js'; - -// Test utilities -function createTestBattleContext() { - const player = makePlayer(); - player.hp = 50; - player.maxHp = 50; - player.energy = 3; - player.maxEnergy = 3; - player.block = 0; - player.weak = 0; - player.vuln = 0; - player.hand = []; - player.deck = []; - player.draw = []; - player.discard = []; - - const enemy = { - id: "test_dummy", - name: "Test Dummy", - hp: 100, - maxHp: 100, - block: 0, - weak: 0, - vuln: 0, - intent: { type: "attack", value: 5 } - }; - - const logs = []; - const ctx = { - player, - enemy, - flags: {}, - lastCard: null, - relicStates: [], - log: (msg) => logs.push(msg), - logs, - render: () => {}, - deal: (target, amount) => { - const blocked = Math.min(amount, target.block); - const damage = Math.max(0, amount - blocked); - target.block -= blocked; - target.hp -= damage; - logs.push(`Deal ${amount} damage (${damage} after ${blocked} block)`); - }, - draw: (n) => { - for (let i = 0; i < n && player.draw.length > 0; i++) { - const cardId = player.draw.pop(); - const card = cloneCard(CARDS[cardId]); - if (card) player.hand.push(card); - } - }, - applyWeak: (target, amount) => { - target.weak = (target.weak || 0) + amount; - logs.push(`Applied ${amount} weak`); - }, - applyVulnerable: (target, amount) => { - target.vuln = (target.vuln || 0) + amount; - logs.push(`Applied ${amount} vulnerable`); - }, - intentIsAttack: () => enemy.intent.type === "attack", - scalarFromWeak: (base) => player.weak > 0 ? Math.floor(base * 0.75) : base, - forceEndTurn: () => logs.push("Turn ended"), - promptExhaust: (count) => { - while (count-- > 0 && player.hand.length > 0) { - const card = player.hand.shift(); - logs.push(`Exhausted ${card.name}`); - } - }, - moveFromDiscardToHand: (cardId) => { - const idx = player.discard.findIndex(id => id === cardId); - if (idx >= 0) { - const [id] = player.discard.splice(idx, 1); - const card = cloneCard(CARDS[id]); - if (card) { - player.hand.push(card); - return true; - } - } - return false; - }, - countCardType: (type) => { - const allCards = [...player.deck, ...player.hand.map(c => c.id), ...player.draw, ...player.discard]; - return allCards.filter(id => CARDS[id]?.type === type).length; - }, - replayCard: (card) => { - if (typeof card.effect === 'function') { - card.effect(ctx); - logs.push(`Replayed ${card.name}`); - } - } - }; - - return ctx; -} - -function testCard(cardId, setupFn = null, assertionFn = null) { - const card = CARDS[cardId]; - if (!card) { - throw new Error(`Card ${cardId} not found`); - } - - const ctx = createTestBattleContext(); - - // Setup test conditions - if (setupFn) { - setupFn(ctx); - } - - // Record initial state - const initialState = { - playerHp: ctx.player.hp, - enemyHp: ctx.enemy.hp, - playerBlock: ctx.player.block, - enemyBlock: ctx.enemy.block, - playerEnergy: ctx.player.energy, - handSize: ctx.player.hand.length, - discardSize: ctx.player.discard.length - }; - - // Clear logs - ctx.logs.length = 0; - - try { - // Execute card effect - card.effect(ctx); - - const finalState = { - playerHp: ctx.player.hp, - enemyHp: ctx.enemy.hp, - playerBlock: ctx.player.block, - enemyBlock: ctx.enemy.block, - playerEnergy: ctx.player.energy, - handSize: ctx.player.hand.length, - discardSize: ctx.player.discard.length - }; - - const result = { - card, - initialState, - finalState, - logs: [...ctx.logs], - flags: { ...ctx.flags }, - success: true, - error: null - }; - - // Run custom assertions - if (assertionFn) { - assertionFn(result, ctx); - } - - return result; - } catch (error) { - return { - card, - initialState, - finalState: initialState, - logs: [...ctx.logs], - flags: { ...ctx.flags }, - success: false, - error: error.message - }; - } -} - -// Test Suite Definitions -const tests = { - // Basic Attack Cards - strike: () => testCard('strike', null, (result) => { - if (result.finalState.enemyHp !== 94) { - throw new Error(`Expected enemy to have 94 HP, got ${result.finalState.enemyHp}`); - } - }), - - 'strike+': () => testCard('strike+', null, (result) => { - if (result.finalState.enemyHp !== 91) { - throw new Error(`Expected enemy to have 91 HP, got ${result.finalState.enemyHp}`); - } - }), - - // Basic Skill Cards - defend: () => testCard('defend', null, (result) => { - if (result.finalState.playerBlock !== 5) { - throw new Error(`Expected player to have 5 block, got ${result.finalState.playerBlock}`); - } - }), - - 'defend+': () => testCard('defend+', null, (result) => { - if (result.finalState.playerBlock !== 8) { - throw new Error(`Expected player to have 8 block, got ${result.finalState.playerBlock}`); - } - }), - - // Energy Cards - coffee_rush: () => testCard('coffee_rush', null, (result) => { - if (result.finalState.playerEnergy !== 5) { - throw new Error(`Expected player to have 5 energy, got ${result.finalState.playerEnergy}`); - } - }), - - 'coffee_rush+': () => testCard('coffee_rush+', null, (result) => { - if (result.finalState.playerEnergy !== 6) { - throw new Error(`Expected player to have 6 energy, got ${result.finalState.playerEnergy}`); - } - }), - - // Complex Cards - macro: () => testCard('macro', - (ctx) => { - ctx.lastCard = 'strike'; - ctx.player.hand.push(cloneCard(CARDS['strike'])); - }, - (result) => { - if (result.finalState.enemyHp !== 94) { - throw new Error(`Macro should have replayed strike, expected enemy HP 94, got ${result.finalState.enemyHp}`); - } - } - ), - - segfault: () => testCard('segfault', - (ctx) => { - ctx.player.draw = ['strike']; - }, - (result) => { - if (result.finalState.enemyHp !== 93) { - throw new Error(`Expected enemy to have 93 HP from segfault, got ${result.finalState.enemyHp}`); - } - if (result.finalState.handSize !== 1) { - throw new Error(`Expected 1 card drawn, hand size is ${result.finalState.handSize}`); - } - } - ), - - skill_issue: () => testCard('skill_issue', - (ctx) => { - ctx.enemy.intent = { type: "attack", value: 5 }; - }, - (result) => { - if (result.finalState.playerBlock !== 6) { - throw new Error(`Expected 6 block, got ${result.finalState.playerBlock}`); - } - if (result.finalState.enemyHp !== 100 || ctx.enemy.weak !== 1) { - throw new Error(`Expected enemy to be weakened when intending attack`); - } - } - ), - - dark_mode: () => testCard('dark_mode', null, (result) => { - if (result.finalState.enemyHp !== 80) { - throw new Error(`Expected enemy to have 80 HP, got ${result.finalState.enemyHp}`); - } - if (!result.logs.includes("Turn ended")) { - throw new Error(`Expected turn to end`); - } - }), - - just_one_game: () => testCard('just_one_game', null, (result) => { - if (!result.flags.skipThisTurn) { - throw new Error(`Expected skipThisTurn flag to be set`); - } - if (result.flags.nextTurnEnergyBonus !== 2) { - throw new Error(`Expected nextTurnEnergyBonus to be 2, got ${result.flags.nextTurnEnergyBonus}`); - } - }), - - vibe_code: () => testCard('vibe_code', null, (result) => { - if (!result.flags.nextCardFree) { - throw new Error(`Expected nextCardFree flag to be set`); - } - }), - - pair_programming: () => testCard('pair_programming', null, (result) => { - if (!result.flags.doubleNextCard) { - throw new Error(`Expected doubleNextCard flag to be set`); - } - }), - - // Cards with conditions - hotfix: () => { - // Test when HP is low (should work) - const lowHpResult = testCard('hotfix', - (ctx) => { - ctx.player.hp = 20; // Below 50% - }, - (result) => { - if (result.finalState.enemyHp !== 90) { - throw new Error(`Expected enemy to have 90 HP when hotfix works, got ${result.finalState.enemyHp}`); - } - } - ); - - return lowHpResult; - }, - - // Advanced mechanics - ctrl_z: () => testCard('ctrl_z', - (ctx) => { - ctx.player.discard = ['strike']; - }, - (result) => { - if (result.finalState.handSize !== 1) { - throw new Error(`Expected 1 card in hand after ctrl_z, got ${result.finalState.handSize}`); - } - if (result.finalState.discardSize !== 0) { - throw new Error(`Expected discard to be empty after ctrl_z, got ${result.finalState.discardSize}`); - } - } - ), - - npm_audit: () => testCard('npm_audit', - (ctx) => { - ctx.player.deck = ['sugar_crash', 'sugar_crash']; - ctx.player.discard = ['sugar_crash']; - }, - (result) => { - if (result.finalState.playerBlock !== 9) { // 3 curses * 3 block each - throw new Error(`Expected 9 block from npm_audit, got ${result.finalState.playerBlock}`); - } - } - ), - - infinite_loop: () => testCard('infinite_loop', - (ctx) => { - ctx.lastCard = 'strike'; - ctx.player.hand.push(cloneCard(CARDS['strike'])); - }, - (result) => { - if (!result.logs.some(log => log.includes('Replayed'))) { - throw new Error(`Expected card to be replayed`); - } - } - ), - - // Curse cards - sugar_crash: () => { - try { - testCard('sugar_crash'); - throw new Error('Sugar crash should not be playable'); - } catch (error) { - if (!error.message.includes('cannot be played')) { - throw new Error(`Expected curse to be unplayable, got: ${error.message}`); - } - } - return { success: true, message: 'Curse correctly unplayable' }; - } -}; - -// Test Runner -function runAllTests() { - console.log('šŸŽ® Running Birthday Spire Card Tests...\n'); - - const results = { - passed: 0, - failed: 0, - errors: [] - }; - - for (const [cardId, testFn] of Object.entries(tests)) { - try { - console.log(`Testing ${cardId}...`); - const result = testFn(); - console.log(`āœ… ${cardId} passed`); - results.passed++; - } catch (error) { - console.log(`āŒ ${cardId} failed: ${error.message}`); - results.failed++; - results.errors.push({ cardId, error: error.message }); - } - } - - console.log(`\nšŸ“Š Test Results:`); - console.log(`āœ… Passed: ${results.passed}`); - console.log(`āŒ Failed: ${results.failed}`); - console.log(`šŸ“ˆ Total: ${results.passed + results.failed}`); - - if (results.errors.length > 0) { - console.log(`\nšŸ” Failed Tests:`); - results.errors.forEach(({ cardId, error }) => { - console.log(` ${cardId}: ${error}`); - }); - } - - return results; -} - -// Comprehensive Test Coverage Check -function checkTestCoverage() { - const allCardIds = Object.keys(CARDS); - const testedCardIds = Object.keys(tests); - const untestedCards = allCardIds.filter(id => !testedCardIds.includes(id)); - - console.log(`\nšŸ“‹ Test Coverage:`); - console.log(`Total cards: ${allCardIds.length}`); - console.log(`Tested cards: ${testedCardIds.length}`); - console.log(`Coverage: ${((testedCardIds.length / allCardIds.length) * 100).toFixed(1)}%`); - - if (untestedCards.length > 0) { - console.log(`\nāš ļø Untested cards:`); - untestedCards.forEach(cardId => console.log(` - ${cardId}`)); - } - - return { - total: allCardIds.length, - tested: testedCardIds.length, - untested: untestedCards - }; -} - -// Export for use in browser or Node.js -if (typeof window !== 'undefined') { - window.CardTests = { runAllTests, checkTestCoverage, testCard }; -} else if (typeof module !== 'undefined') { - module.exports = { runAllTests, checkTestCoverage, testCard }; -} - -// Auto-run if called directly -if (typeof window === 'undefined' && import.meta.url === `file://${process.argv[1]}`) { - runAllTests(); - checkTestCoverage(); -} diff --git a/tests/run-tests.js b/tests/run-tests.js deleted file mode 100644 index 921cc74..0000000 --- a/tests/run-tests.js +++ /dev/null @@ -1,678 +0,0 @@ -#!/usr/bin/env node - -/** - * Node.js Test Runner for Birthday Spire Card Tests - * Usage: node tests/run-tests.js [--card=cardId] [--coverage] [--verbose] - */ - -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Mock browser environment for Node.js -global.window = {}; - -// Import game modules (adjust paths as needed) -let CARDS, ENEMIES, makePlayer, cloneCard; - -try { - const cardsModule = await import('../src/data/cards.js'); - const enemiesModule = await import('../src/data/enemies.js'); - const coreModule = await import('../src/engine/core.js'); - - CARDS = cardsModule.CARDS; - ENEMIES = enemiesModule.ENEMIES; - makePlayer = coreModule.makePlayer; - cloneCard = coreModule.cloneCard; -} catch (error) { - console.error('āŒ Failed to import game modules:', error.message); - console.error('Make sure you\'re running this from the project root directory'); - process.exit(1); -} - -// Test utilities -function createTestBattleContext() { - const player = makePlayer(); - player.hp = 50; - player.maxHp = 50; - player.energy = 3; - player.maxEnergy = 3; - player.block = 0; - player.weak = 0; - player.vuln = 0; - player.hand = []; - player.deck = []; - player.draw = []; - player.discard = []; - - const enemy = { - id: "test_dummy", - name: "Test Dummy", - hp: 100, - maxHp: 100, - block: 0, - weak: 0, - vuln: 0, - intent: { type: "attack", value: 5 } - }; - - const logs = []; - const ctx = { - player, - enemy, - flags: {}, - lastCard: null, - relicStates: [], - log: (msg) => logs.push(msg), - logs, - render: () => {}, - deal: (target, amount) => { - const blocked = Math.min(amount, target.block); - const damage = Math.max(0, amount - blocked); - target.block -= blocked; - target.hp -= damage; - logs.push(`Deal ${amount} damage (${damage} after ${blocked} block)`); - }, - draw: (n) => { - for (let i = 0; i < n && player.draw.length > 0; i++) { - const cardId = player.draw.pop(); - const card = cloneCard(CARDS[cardId]); - if (card) player.hand.push(card); - } - }, - applyWeak: (target, amount) => { - target.weak = (target.weak || 0) + amount; - logs.push(`Applied ${amount} weak`); - }, - applyVulnerable: (target, amount) => { - target.vuln = (target.vuln || 0) + amount; - logs.push(`Applied ${amount} vulnerable`); - }, - intentIsAttack: () => enemy.intent.type === "attack", - scalarFromWeak: (base) => player.weak > 0 ? Math.floor(base * 0.75) : base, - forceEndTurn: () => logs.push("Turn ended"), - promptExhaust: (count) => { - while (count-- > 0 && player.hand.length > 0) { - const card = player.hand.shift(); - logs.push(`Exhausted ${card.name}`); - } - }, - moveFromDiscardToHand: (cardId) => { - const idx = player.discard.findIndex(id => id === cardId); - if (idx >= 0) { - const [id] = player.discard.splice(idx, 1); - const card = cloneCard(CARDS[id]); - if (card) { - player.hand.push(card); - return true; - } - } - return false; - }, - countCardType: (type) => { - const allCards = [...player.deck, ...player.hand.map(c => c.id), ...player.draw, ...player.discard]; - return allCards.filter(id => CARDS[id]?.type === type).length; - }, - replayCard: (card) => { - if (typeof card.effect === 'function') { - card.effect(ctx); - logs.push(`Replayed ${card.name}`); - } - } - }; - - return ctx; -} - -function testCard(cardId, setupFn = null, assertionFn = null) { - const card = CARDS[cardId]; - if (!card) { - throw new Error(`Card ${cardId} not found`); - } - - const ctx = createTestBattleContext(); - - if (setupFn) { - setupFn(ctx); - } - - const initialState = { - playerHp: ctx.player.hp, - enemyHp: ctx.enemy.hp, - playerBlock: ctx.player.block, - enemyBlock: ctx.enemy.block, - playerEnergy: ctx.player.energy, - handSize: ctx.player.hand.length, - discardSize: ctx.player.discard.length - }; - - ctx.logs.length = 0; - - try { - card.effect(ctx); - - const finalState = { - playerHp: ctx.player.hp, - enemyHp: ctx.enemy.hp, - playerBlock: ctx.player.block, - enemyBlock: ctx.enemy.block, - playerEnergy: ctx.player.energy, - handSize: ctx.player.hand.length, - discardSize: ctx.player.discard.length - }; - - const result = { - card, - initialState, - finalState, - logs: [...ctx.logs], - flags: { ...ctx.flags }, - success: true, - error: null - }; - - if (assertionFn) { - assertionFn(result, ctx); - } - - return result; - } catch (error) { - return { - card, - initialState, - finalState: initialState, - logs: [...ctx.logs], - flags: { ...ctx.flags }, - success: false, - error: error.message - }; - } -} - -// Comprehensive test definitions -const tests = { - // Basic cards - strike: () => testCard('strike', null, (result) => { - if (result.finalState.enemyHp !== 94) { - throw new Error(`Expected enemy HP 94, got ${result.finalState.enemyHp}`); - } - }), - - 'strike+': () => testCard('strike+', null, (result) => { - if (result.finalState.enemyHp !== 91) { - throw new Error(`Expected enemy HP 91, got ${result.finalState.enemyHp}`); - } - }), - - defend: () => testCard('defend', null, (result) => { - if (result.finalState.playerBlock !== 5) { - throw new Error(`Expected player block 5, got ${result.finalState.playerBlock}`); - } - }), - - 'defend+': () => testCard('defend+', null, (result) => { - if (result.finalState.playerBlock !== 8) { - throw new Error(`Expected player block 8, got ${result.finalState.playerBlock}`); - } - }), - - // Energy cards - coffee_rush: () => testCard('coffee_rush', null, (result) => { - if (result.finalState.playerEnergy !== 5) { - throw new Error(`Expected energy 5, got ${result.finalState.playerEnergy}`); - } - }), - - 'coffee_rush+': () => testCard('coffee_rush+', null, (result) => { - if (result.finalState.playerEnergy !== 6) { - throw new Error(`Expected energy 6, got ${result.finalState.playerEnergy}`); - } - }), - - // Complex cards - macro: () => testCard('macro', - (ctx) => { - ctx.lastCard = 'strike'; - ctx.player.hand.push(cloneCard(CARDS['strike'])); - }, - (result) => { - if (result.finalState.enemyHp !== 94) { - throw new Error(`Macro should replay strike, expected enemy HP 94, got ${result.finalState.enemyHp}`); - } - } - ), - - segfault: () => testCard('segfault', - (ctx) => { - ctx.player.draw = ['strike']; - }, - (result) => { - if (result.finalState.enemyHp !== 93) { - throw new Error(`Expected enemy HP 93, got ${result.finalState.enemyHp}`); - } - if (result.finalState.handSize !== 1) { - throw new Error(`Expected hand size 1, got ${result.finalState.handSize}`); - } - } - ), - - skill_issue: () => testCard('skill_issue', - (ctx) => { - ctx.enemy.intent = { type: "attack", value: 5 }; - }, - (result, ctx) => { - if (result.finalState.playerBlock !== 6) { - throw new Error(`Expected block 6, got ${result.finalState.playerBlock}`); - } - if (ctx.enemy.weak !== 1) { - throw new Error(`Expected enemy to be weakened`); - } - } - ), - - dark_mode: () => testCard('dark_mode', null, (result) => { - if (result.finalState.enemyHp !== 80) { - throw new Error(`Expected enemy HP 80, got ${result.finalState.enemyHp}`); - } - if (!result.logs.includes("Turn ended")) { - throw new Error(`Expected turn to end`); - } - }), - - // Flag-based cards - just_one_game: () => testCard('just_one_game', null, (result) => { - if (!result.flags.skipThisTurn) { - throw new Error(`Expected skipThisTurn flag`); - } - if (result.flags.nextTurnEnergyBonus !== 2) { - throw new Error(`Expected nextTurnEnergyBonus 2, got ${result.flags.nextTurnEnergyBonus}`); - } - }), - - vibe_code: () => testCard('vibe_code', null, (result) => { - if (!result.flags.nextCardFree) { - throw new Error(`Expected nextCardFree flag`); - } - }), - - pair_programming: () => testCard('pair_programming', null, (result) => { - if (!result.flags.doubleNextCard) { - throw new Error(`Expected doubleNextCard flag`); - } - }), - - // Advanced mechanics - ctrl_z: () => testCard('ctrl_z', - (ctx) => { - ctx.player.discard = ['strike']; - }, - (result) => { - if (result.finalState.handSize !== 1) { - throw new Error(`Expected hand size 1, got ${result.finalState.handSize}`); - } - if (result.finalState.discardSize !== 0) { - throw new Error(`Expected discard size 0, got ${result.finalState.discardSize}`); - } - } - ), - - npm_audit: () => testCard('npm_audit', - (ctx) => { - ctx.player.deck = ['sugar_crash', 'sugar_crash']; - ctx.player.discard = ['sugar_crash']; - }, - (result) => { - if (result.finalState.playerBlock !== 9) { - throw new Error(`Expected block 9 (3 curses * 3), got ${result.finalState.playerBlock}`); - } - } - ), - - infinite_loop: () => testCard('infinite_loop', - (ctx) => { - ctx.lastCard = 'strike'; - ctx.player.hand.push(cloneCard(CARDS['strike'])); - }, - (result) => { - if (!result.logs.some(log => log.includes('Replayed'))) { - throw new Error(`Expected card to be replayed`); - } - } - ), - - // Test all basic damage cards work - merge_conflict: () => testCard('merge_conflict', null, (result) => { - if (result.finalState.enemyHp !== 88) { // 6 + 6 = 12 damage - throw new Error(`Expected enemy HP 88, got ${result.finalState.enemyHp}`); - } - }), - - production_deploy: () => testCard('production_deploy', null, (result) => { - if (result.finalState.enemyHp !== 75) { // 25 damage - throw new Error(`Expected enemy HP 75, got ${result.finalState.enemyHp}`); - } - if (result.finalState.playerHp !== 45) { // Lost 5 HP - throw new Error(`Expected player HP 45, got ${result.finalState.playerHp}`); - } - }), - - // Test healing cards - stack_trace: () => testCard('stack_trace', - (ctx) => { - ctx.player.hp = 40; // Start damaged - }, - (result) => { - if (result.finalState.playerHp !== 45) { // Healed 5 - throw new Error(`Expected player HP 45, got ${result.finalState.playerHp}`); - } - } - ), - - refactor: () => testCard('refactor', - (ctx) => { - ctx.player.hp = 40; - ctx.player.draw = ['strike']; - }, - (result) => { - if (result.finalState.playerHp !== 48) { // Healed 8 - throw new Error(`Expected player HP 48, got ${result.finalState.playerHp}`); - } - if (result.finalState.handSize !== 1) { // Drew 1 card - throw new Error(`Expected hand size 1, got ${result.finalState.handSize}`); - } - } - ), - - // Previously untested cards - '404': () => testCard('404', null, (result, ctx) => { - if (ctx.enemy.weak !== 2) { - throw new Error(`Expected enemy to have 2 weak, got ${ctx.enemy.weak}`); - } - }), - - object_object: () => testCard('object_object', - (ctx) => { - ctx.player.hand = [cloneCard(CARDS['strike']), cloneCard(CARDS['defend'])]; - ctx.player.draw = ['coffee_rush']; - }, - (result) => { - if (result.finalState.handSize !== 2) { // Exhausted 1, drew 2, net +1 - throw new Error(`Expected hand size 2, got ${result.finalState.handSize}`); - } - } - ), - - colon_q: () => testCard('colon_q', - (ctx) => { - ctx.player.discard = ['strike', 'defend', 'coffee_rush']; // 3 cards - }, - (result) => { - if (result.finalState.enemyHp !== 97) { // 100 - 3 = 97 - throw new Error(`Expected enemy HP 97, got ${result.finalState.enemyHp}`); - } - } - ), - - raw_dog: () => testCard('raw_dog', - (ctx) => { - ctx.player.draw = ['strike', 'defend']; - }, - (result) => { - if (result.finalState.handSize !== 2) { - throw new Error(`Expected hand size 2, got ${result.finalState.handSize}`); - } - // Note: Card should be exhausted after play - } - ), - - task_failed_successfully: () => testCard('task_failed_successfully', null, (result) => { - if (result.finalState.enemyHp !== 88) { // 8 + 4 = 12 damage (no block) - throw new Error(`Expected enemy HP 88, got ${result.finalState.enemyHp}`); - } - }), - - recursion: () => testCard('recursion', null, (result) => { - if (result.finalState.enemyHp !== 90) { // 5 + 5 = 10 damage (triggers twice) - throw new Error(`Expected enemy HP 90, got ${result.finalState.enemyHp}`); - } - }), - - git_commit: () => testCard('git_commit', - (ctx) => { - ctx.player.draw = ['strike']; // Need cards to draw - }, - (result) => { - if (result.finalState.playerBlock !== 4) { // Actually gives 4 block, not 8 - throw new Error(`Expected player block 4, got ${result.finalState.playerBlock}`); - } - if (result.finalState.handSize !== 1) { // Should draw 1 card - throw new Error(`Expected hand size 1, got ${result.finalState.handSize}`); - } - } - ), - - memory_leak: () => testCard('memory_leak', null, (result) => { - if (result.finalState.playerEnergy !== 4) { // +1 energy - throw new Error(`Expected player energy 4, got ${result.finalState.playerEnergy}`); - } - if (result.flags.nextTurnEnergyPenalty !== 1) { - throw new Error(`Expected nextTurnEnergyPenalty 1, got ${result.flags.nextTurnEnergyPenalty}`); - } - }), - - code_review: () => testCard('code_review', - (ctx) => { - ctx.player.draw = ['strike']; // Need cards to draw - }, - (result) => { - if (result.finalState.handSize !== 1) { // Should draw 1 card - throw new Error(`Expected hand size 1, got ${result.finalState.handSize}`); - } - if (!result.logs.includes('Code review reveals useful insights. You draw a card.')) { - throw new Error(`Expected code review log message`); - } - } - ), - - hotfix: () => testCard('hotfix', - (ctx) => { - ctx.player.hp = 20; // Below 50% - }, - (result) => { - if (result.finalState.enemyHp !== 90) { - throw new Error(`Expected enemy HP 90, got ${result.finalState.enemyHp}`); - } - } - ), - - ligma: () => testCard('ligma', - (ctx) => { - ctx.player.draw = ['strike']; // Need cards to draw - }, - (result) => { - if (result.finalState.playerHp !== 0) { // Takes exactly 69 damage, 50-69=0 (clamped to 0) - throw new Error(`Expected player HP 0 after 69 damage, got ${result.finalState.playerHp}`); - } - if (result.finalState.handSize !== 1) { // Should draw 1 card - throw new Error(`Expected hand size 1, got ${result.finalState.handSize}`); - } - } - ), - - virgin: () => testCard('virgin', - (ctx) => { - ctx.enemy.intent = { type: "attack", value: 5 }; - }, - (result) => { - if (result.finalState.playerBlock !== 8) { - throw new Error(`Expected player block 8, got ${result.finalState.playerBlock}`); - } - } - ), - - sugar_crash: () => { - // Test that curse cards cannot be played - const ctx = createTestBattleContext(); - const card = CARDS['sugar_crash']; - - try { - card.effect(ctx); - // If we get here, the card was played successfully - // But curse cards should be blocked at the game level, not in the effect - // So this test just verifies the effect exists and doesn't crash - return { success: true, message: 'Curse effect executed' }; - } catch (error) { - throw new Error(`Unexpected error in curse effect: ${error.message}`); - } - }, - - stack_overflow: () => testCard('stack_overflow', - (ctx) => { - ctx.player.hand = [cloneCard(CARDS['strike']), cloneCard(CARDS['defend'])]; // 2 cards in hand - }, - (result) => { - if (result.finalState.enemyHp !== 98) { // 2 damage - throw new Error(`Expected enemy HP 98, got ${result.finalState.enemyHp}`); - } - } - ), - - rubber_duck: () => testCard('rubber_duck', - (ctx) => { - ctx.player.draw = ['strike', 'defend']; - }, - (result) => { - if (result.finalState.handSize !== 3) { // Drew 3 cards - throw new Error(`Expected hand size 3, got ${result.finalState.handSize}`); - } - } - ), - - git_push_force: () => testCard('git_push_force', - (ctx) => { - ctx.player.hand = [cloneCard(CARDS['strike']), cloneCard(CARDS['defend'])]; - }, - (result, ctx) => { - if (result.finalState.enemyHp !== 85) { // 15 damage - throw new Error(`Expected enemy HP 85, got ${result.finalState.enemyHp}`); - } - // Should have moved a card back to draw pile - if (result.finalState.handSize !== 1) { // Started with 2, removed 1 - throw new Error(`Expected hand size 1 after force push, got ${result.finalState.handSize}`); - } - } - ) -}; - -// Test runner -function runAllTests(verbose = false) { - console.log('šŸŽ® Running Birthday Spire Card Tests...\n'); - - const results = { - passed: 0, - failed: 0, - errors: [] - }; - - for (const [cardId, testFn] of Object.entries(tests)) { - try { - if (verbose) console.log(`Testing ${cardId}...`); - const result = testFn(); - console.log(`āœ… ${cardId} passed`); - results.passed++; - - if (verbose && result.logs.length > 0) { - console.log(` Logs: ${result.logs.join(', ')}`); - } - } catch (error) { - console.log(`āŒ ${cardId} failed: ${error.message}`); - results.failed++; - results.errors.push({ cardId, error: error.message }); - } - } - - console.log(`\nšŸ“Š Test Results:`); - console.log(`āœ… Passed: ${results.passed}`); - console.log(`āŒ Failed: ${results.failed}`); - console.log(`šŸ“ˆ Total: ${results.passed + results.failed}`); - - if (results.errors.length > 0) { - console.log(`\nšŸ” Failed Tests:`); - results.errors.forEach(({ cardId, error }) => { - console.log(` ${cardId}: ${error}`); - }); - } - - return results; -} - -function checkTestCoverage() { - const allCardIds = Object.keys(CARDS); - const testedCardIds = Object.keys(tests); - const untestedCards = allCardIds.filter(id => !testedCardIds.includes(id)); - - console.log(`\nšŸ“‹ Test Coverage:`); - console.log(`Total cards: ${allCardIds.length}`); - console.log(`Tested cards: ${testedCardIds.length}`); - console.log(`Coverage: ${((testedCardIds.length / allCardIds.length) * 100).toFixed(1)}%`); - - if (untestedCards.length > 0) { - console.log(`\nāš ļø Untested cards:`); - untestedCards.forEach(cardId => console.log(` - ${cardId}`)); - } - - return { - total: allCardIds.length, - tested: testedCardIds.length, - untested: untestedCards - }; -} - -function runSpecificCardTest(cardId, verbose = false) { - if (!CARDS[cardId]) { - console.log(`āŒ Card '${cardId}' not found`); - return; - } - - console.log(`šŸŽÆ Testing specific card: ${cardId}`); - - try { - const result = testCard(cardId); - console.log(`āœ… ${cardId} executed successfully`); - - if (verbose) { - console.log(`Initial state:`, result.initialState); - console.log(`Final state:`, result.finalState); - console.log(`Logs:`, result.logs); - if (Object.keys(result.flags).length > 0) { - console.log(`Flags:`, result.flags); - } - } - } catch (error) { - console.log(`āŒ ${cardId} failed: ${error.message}`); - } -} - -// CLI handling -const args = process.argv.slice(2); -const cardArg = args.find(arg => arg.startsWith('--card=')); -const coverageArg = args.includes('--coverage'); -const verboseArg = args.includes('--verbose'); - -if (cardArg) { - const cardId = cardArg.split('=')[1]; - runSpecificCardTest(cardId, verboseArg); -} else if (coverageArg) { - checkTestCoverage(); -} else { - const results = runAllTests(verboseArg); - checkTestCoverage(); - - // Exit with error code if tests failed - if (results.failed > 0) { - process.exit(1); - } -} diff --git a/tests/test-runner.html b/tests/test-runner.html deleted file mode 100644 index ce1fa23..0000000 --- a/tests/test-runner.html +++ /dev/null @@ -1,501 +0,0 @@ - - - - - - Birthday Spire Card Tests - - - -
-

šŸŽ® Birthday Spire Card Tests

- -
- - - - -
- - - -
Ready to run tests...
- - -
- - - - -