You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
501 lines
18 KiB
501 lines
18 KiB
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Birthday Spire Card Tests</title> |
|
<style> |
|
body { |
|
font-family: 'Courier New', monospace; |
|
max-width: 1200px; |
|
margin: 0 auto; |
|
padding: 20px; |
|
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); |
|
color: white; |
|
min-height: 100vh; |
|
} |
|
|
|
.container { |
|
background: rgba(0, 0, 0, 0.7); |
|
padding: 30px; |
|
border-radius: 10px; |
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); |
|
} |
|
|
|
h1 { |
|
text-align: center; |
|
color: #ffd700; |
|
margin-bottom: 30px; |
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); |
|
} |
|
|
|
.controls { |
|
text-align: center; |
|
margin-bottom: 30px; |
|
} |
|
|
|
button { |
|
background: linear-gradient(135deg, #ff6b6b, #ee5a52); |
|
border: none; |
|
color: white; |
|
padding: 12px 24px; |
|
margin: 0 10px; |
|
border-radius: 5px; |
|
cursor: pointer; |
|
font-size: 16px; |
|
font-weight: bold; |
|
transition: transform 0.2s; |
|
} |
|
|
|
button:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); |
|
} |
|
|
|
button:disabled { |
|
background: #666; |
|
cursor: not-allowed; |
|
transform: none; |
|
} |
|
|
|
.results { |
|
background: rgba(0, 0, 0, 0.5); |
|
padding: 20px; |
|
border-radius: 5px; |
|
margin-top: 20px; |
|
border-left: 4px solid #ffd700; |
|
} |
|
|
|
.test-output { |
|
background: #000; |
|
color: #00ff00; |
|
padding: 20px; |
|
border-radius: 5px; |
|
font-family: 'Courier New', monospace; |
|
font-size: 14px; |
|
line-height: 1.4; |
|
max-height: 600px; |
|
overflow-y: auto; |
|
white-space: pre-wrap; |
|
margin-top: 20px; |
|
} |
|
|
|
.stats { |
|
display: flex; |
|
justify-content: space-around; |
|
margin: 20px 0; |
|
text-align: center; |
|
} |
|
|
|
.stat { |
|
background: rgba(255, 255, 255, 0.1); |
|
padding: 15px; |
|
border-radius: 5px; |
|
flex: 1; |
|
margin: 0 10px; |
|
} |
|
|
|
.stat-value { |
|
font-size: 24px; |
|
font-weight: bold; |
|
color: #ffd700; |
|
} |
|
|
|
.stat-label { |
|
font-size: 14px; |
|
opacity: 0.8; |
|
} |
|
|
|
.error-details { |
|
background: rgba(255, 0, 0, 0.1); |
|
border: 1px solid #ff4444; |
|
padding: 15px; |
|
border-radius: 5px; |
|
margin-top: 10px; |
|
} |
|
|
|
.success { color: #00ff00; } |
|
.error { color: #ff4444; } |
|
.warning { color: #ffaa00; } |
|
|
|
.loading { |
|
text-align: center; |
|
color: #ffd700; |
|
font-style: italic; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<h1>🎮 Birthday Spire Card Tests</h1> |
|
|
|
<div class="controls"> |
|
<button onclick="runTests()" id="runBtn">Run All Tests</button> |
|
<button onclick="checkCoverage()" id="coverageBtn">Check Coverage</button> |
|
<button onclick="runSpecificTest()" id="specificBtn">Test Specific Card</button> |
|
<button onclick="clearOutput()" id="clearBtn">Clear Output</button> |
|
</div> |
|
|
|
<div class="stats" id="stats" style="display: none;"> |
|
<div class="stat"> |
|
<div class="stat-value" id="passedCount">0</div> |
|
<div class="stat-label">Passed</div> |
|
</div> |
|
<div class="stat"> |
|
<div class="stat-value" id="failedCount">0</div> |
|
<div class="stat-label">Failed</div> |
|
</div> |
|
<div class="stat"> |
|
<div class="stat-value" id="coveragePercent">0%</div> |
|
<div class="stat-label">Coverage</div> |
|
</div> |
|
</div> |
|
|
|
<div class="test-output" id="output">Ready to run tests...</div> |
|
|
|
<div id="errorDetails" class="error-details" style="display: none;"> |
|
<h3>Failed Tests:</h3> |
|
<div id="errorList"></div> |
|
</div> |
|
</div> |
|
|
|
<!-- Import the game modules --> |
|
<script type="module"> |
|
import { CARDS, STARTER_DECK, CARD_POOL } from '../src/data/cards.js'; |
|
import { ENEMIES } from '../src/data/enemies.js'; |
|
import { makePlayer, cloneCard } from '../src/engine/core.js'; |
|
|
|
// Make available globally for the test runner |
|
window.CARDS = CARDS; |
|
window.ENEMIES = ENEMIES; |
|
window.makePlayer = makePlayer; |
|
window.cloneCard = cloneCard; |
|
|
|
// Simple console override to capture output |
|
const originalConsole = { ...console }; |
|
let capturedOutput = []; |
|
|
|
function captureConsole() { |
|
capturedOutput = []; |
|
console.log = (...args) => { |
|
capturedOutput.push(args.join(' ')); |
|
originalConsole.log(...args); |
|
}; |
|
} |
|
|
|
function restoreConsole() { |
|
Object.assign(console, originalConsole); |
|
} |
|
|
|
function output(text, className = '') { |
|
const outputEl = document.getElementById('output'); |
|
const span = document.createElement('span'); |
|
span.className = className; |
|
span.textContent = text + '\n'; |
|
outputEl.appendChild(span); |
|
outputEl.scrollTop = outputEl.scrollHeight; |
|
} |
|
|
|
function clearOutput() { |
|
document.getElementById('output').innerHTML = 'Output cleared...\n'; |
|
document.getElementById('stats').style.display = 'none'; |
|
document.getElementById('errorDetails').style.display = 'none'; |
|
} |
|
|
|
function updateStats(results) { |
|
document.getElementById('passedCount').textContent = results.passed; |
|
document.getElementById('failedCount').textContent = results.failed; |
|
document.getElementById('stats').style.display = 'flex'; |
|
|
|
if (results.errors && results.errors.length > 0) { |
|
const errorDetails = document.getElementById('errorDetails'); |
|
const errorList = document.getElementById('errorList'); |
|
errorList.innerHTML = ''; |
|
|
|
results.errors.forEach(({ cardId, error }) => { |
|
const errorDiv = document.createElement('div'); |
|
errorDiv.innerHTML = `<strong>${cardId}:</strong> ${error}`; |
|
errorList.appendChild(errorDiv); |
|
}); |
|
|
|
errorDetails.style.display = 'block'; |
|
} |
|
} |
|
|
|
// Test utilities (simplified version for browser) |
|
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, |
|
playerEnergy: ctx.player.energy, |
|
handSize: ctx.player.hand.length |
|
}; |
|
|
|
ctx.logs.length = 0; |
|
|
|
try { |
|
card.effect(ctx); |
|
|
|
const finalState = { |
|
playerHp: ctx.player.hp, |
|
enemyHp: ctx.enemy.hp, |
|
playerBlock: ctx.player.block, |
|
playerEnergy: ctx.player.energy, |
|
handSize: ctx.player.hand.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 |
|
}; |
|
} |
|
} |
|
|
|
// Sample tests |
|
const sampleTests = { |
|
strike: () => testCard('strike', null, (result) => { |
|
if (result.finalState.enemyHp !== 94) { |
|
throw new Error(`Expected enemy to have 94 HP, got ${result.finalState.enemyHp}`); |
|
} |
|
}), |
|
|
|
defend: () => testCard('defend', null, (result) => { |
|
if (result.finalState.playerBlock !== 5) { |
|
throw new Error(`Expected player to have 5 block, got ${result.finalState.playerBlock}`); |
|
} |
|
}), |
|
|
|
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}`); |
|
} |
|
}), |
|
|
|
segfault: () => testCard('segfault', |
|
(ctx) => { |
|
ctx.player.draw = ['strike']; |
|
}, |
|
(result) => { |
|
if (result.finalState.enemyHp !== 93) { |
|
throw new Error(`Expected enemy to have 93 HP, got ${result.finalState.enemyHp}`); |
|
} |
|
if (result.finalState.handSize !== 1) { |
|
throw new Error(`Expected 1 card drawn, got ${result.finalState.handSize}`); |
|
} |
|
} |
|
), |
|
|
|
pair_programming: () => testCard('pair_programming', null, (result) => { |
|
if (!result.flags.doubleNextCard) { |
|
throw new Error(`Expected doubleNextCard flag to be set`); |
|
} |
|
}) |
|
}; |
|
|
|
window.runTests = async function() { |
|
clearOutput(); |
|
output('🎮 Running Birthday Spire Card Tests...', 'success'); |
|
output(''); |
|
|
|
const results = { |
|
passed: 0, |
|
failed: 0, |
|
errors: [] |
|
}; |
|
|
|
for (const [cardId, testFn] of Object.entries(sampleTests)) { |
|
try { |
|
output(`Testing ${cardId}...`); |
|
const result = testFn(); |
|
output(`✅ ${cardId} passed`, 'success'); |
|
results.passed++; |
|
} catch (error) { |
|
output(`❌ ${cardId} failed: ${error.message}`, 'error'); |
|
results.failed++; |
|
results.errors.push({ cardId, error: error.message }); |
|
} |
|
} |
|
|
|
output(''); |
|
output(`📊 Test Results:`, 'warning'); |
|
output(`✅ Passed: ${results.passed}`, 'success'); |
|
output(`❌ Failed: ${results.failed}`, 'error'); |
|
output(`📈 Total: ${results.passed + results.failed}`); |
|
|
|
updateStats(results); |
|
}; |
|
|
|
window.checkCoverage = function() { |
|
const allCardIds = Object.keys(CARDS); |
|
const testedCardIds = Object.keys(sampleTests); |
|
const coverage = ((testedCardIds.length / allCardIds.length) * 100).toFixed(1); |
|
|
|
clearOutput(); |
|
output('📋 Test Coverage Analysis:', 'warning'); |
|
output(`Total cards: ${allCardIds.length}`); |
|
output(`Sample tested cards: ${testedCardIds.length}`); |
|
output(`Sample coverage: ${coverage}%`); |
|
output(''); |
|
output('Note: This is a sample test suite. Full test suite is in card-tests.js', 'warning'); |
|
|
|
document.getElementById('coveragePercent').textContent = coverage + '%'; |
|
document.getElementById('stats').style.display = 'flex'; |
|
}; |
|
|
|
window.runSpecificTest = function() { |
|
const cardId = prompt('Enter card ID to test:'); |
|
if (!cardId) return; |
|
|
|
if (!CARDS[cardId]) { |
|
output(`❌ Card '${cardId}' not found`, 'error'); |
|
return; |
|
} |
|
|
|
clearOutput(); |
|
output(`Testing specific card: ${cardId}`, 'warning'); |
|
|
|
try { |
|
const result = testCard(cardId); |
|
output(`✅ ${cardId} executed successfully`, 'success'); |
|
output(`Initial state: ${JSON.stringify(result.initialState, null, 2)}`); |
|
output(`Final state: ${JSON.stringify(result.finalState, null, 2)}`); |
|
output(`Logs: ${result.logs.join(', ')}`); |
|
if (Object.keys(result.flags).length > 0) { |
|
output(`Flags: ${JSON.stringify(result.flags, null, 2)}`); |
|
} |
|
} catch (error) { |
|
output(`❌ ${cardId} failed: ${error.message}`, 'error'); |
|
} |
|
}; |
|
|
|
// Make functions global |
|
window.clearOutput = clearOutput; |
|
</script> |
|
</body> |
|
</html>
|
|
|