Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/mbianchidev/2dnd --skill testing명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SKILL.md 표시 중
Develop 2D&D features with Phaser 4, strict TypeScript, and D&D-inspired mechanics
Manage 2D&D save schema v16, migration, normalization, and location recovery
Extend the multi-chunk world, city districts, dungeon levels, terrain, and fog in 2D&D
| name | testing |
| description | Test 2D&D logic with Vitest and browser flows with Playwright |
| license | MIT |
Write comprehensive game-mechanics tests with Vitest. Use the committed Playwright suite only for high-value Phaser browser flows that cannot be proven through pure logic tests.
✅ Dice probability distributions
✅ Combat calculation accuracy
✅ Group initiative, formation, targeting, synergies, rewards, and encounter budgets
✅ Player stat progression
✅ XP and leveling formulas
✅ Quest objective counters, prerequisites, rewards, gates, danger rules, and
save normalization
✅ Companion definitions, party progression, transfers, KO/reward rules, and
ranked gambit selection/normalization
✅ Data integrity (monsters, spells, items)
✅ Monster family references, membership, variant differentiation, palette
uniqueness, encounter weights, Codex family completion, and texture-key coverage
✅ Codex knowledge IDs, canonical references, every unlock signal, idempotency,
normalization/migration, search/sort/group behavior, and future-hook isolation
✅ Game logic functions
✅ Seeded layouts and persistent state machines
✅ Non-combat skill-check math, outcomes, and save normalization
✅ Scene-transition event ordering, duplicate guards, and watchdog recovery with
mocked camera/time adapters
✅ Cutscene data integrity, trigger priority, queue recovery, replay immutability,
shared preference normalization/migration, and director cleanup
✅ Tutorial content integrity, semantic control references, completion
normalization, progression-aware tip unlocks, and save migration
✅ Defeat penalty receipts, once-only Battle resolution, recovered save
round-trips, result-scene continuation, and random/boss parity
✅ Animation state selection, reduced-motion timing, stable actor/target mapping,
once-only completion and cleanup, and family-frame fallback resolution
✅ Semantic mappings, context priority, analog dead zones, repeats, source
switching, duplicate suppression, disconnect/blur cleanup, and control
preference migration
✅ Alignment boundaries, reputation tiers, clamping, stable-source
idempotency, quest/dialogue/event/trap/combat hooks, shop composition, Codex
milestones, schema-v12 migration, bounded history, and debug commands
✅ Achievement definition integrity, deterministic progress, hidden entries,
idempotent battle/event/social hooks, reconciliation, schema-v13 migration,
one-hit/no-defeat rules, cosmetic titles, debug exclusion, notices, and
authority isolation
✅ Gathering terrain safety, seeded tables, all three state machines,
reduced-motion equivalence, cooldowns, environmental weights, economy, guarded
Battle handoffs, recipe-input isolation, schema-v14 migration, and pending reload
✅ Crafting recipe integrity, alternative material matching, protected/equipped
items, atomic batches, repeated-input suppression, equipment-link upgrades,
ownership, discovery, acquisition tables, anti-arbitrage, achievements, debug
exclusion, authority isolation, schema-v15 migration, and corruption repair
✅ Nautical continent/zone/port/island integrity, pre-boat merchant routes,
boat acquisition/upgrades, embark/disembark safety, sea fog, capped encounters,
weather hazards, Kraken hooks, sea gathering/crafting economy, schema-v16
migration/corruption recovery, debug commands, and browser reload while sailing
❌ Phaser rendering/graphics ❌ Scene transitions ❌ User input handling ❌ Visual appearance ❌ Animation timing
These exclusions apply to Vitest. The focused e2e/ suite owns the real-browser
campaign golden path and scene/input integration.
Animation browser coverage should assert debug-state evidence from real action menus and combat turns for hero, companion, monster, and boss presentation; spell/ability particles; world/follower/mount gait; boss cutscenes; fainting; reduced-motion immediate states; and page/console cleanliness.
Accessibility browser coverage should change settings from both title and
in-game surfaces, assert immediate canvas state, exercise core overlays at every
supported text scale, verify reload persistence, and prove 2dnd_save is
unchanged by preference updates.
Semantic-controls browser coverage uses mobile/touch emulation and deterministic standard-gamepad stubs. It covers onboarding and mobile text entry, movement, overlays, prompt/source switching, Battle action/target confirmation, defeat recovery, gamepad cursor visibility, portrait/landscape layout, and page/console cleanliness.
Codex browser coverage follows real exploration, quest, cutscene, item, NPC, and readable paths; verifies non-blocking feedback, reload persistence, schema-v9 migration, keyboard/touch/gamepad search and filters, 150% text, reduced motion, and page/console cleanliness.
tests/
├── dice.test.ts # Dice rolling utilities
├── combat.test.ts # Combat mechanics
├── groupCombat.test.ts # Multi-monster combat rules
├── battleActions.test.ts # Pure gambit planning and validation
├── monsterGroups.test.ts # Group templates and generation
├── monsterFamilies.test.ts # Families, variants, Codex derivation, texture keys
├── codexKnowledge.test.ts # World knowledge data, triggers, queries, and hooks
├── partyCombat.test.ts # Stable actor IDs, ally scopes, AI, and result hooks
├── party.test.ts # Recruitment, progression, transfers, rests, KO/rewards
├── companions.test.ts # Immutable companion definitions and loadouts
├── gambits.test.ts # Ranked conditions/actions/targets and normalization
├── followers.test.ts # Pure non-blocking follower trail updates
├── player.test.ts # Player systems
├── traps.test.ts # Dungeon trap placement, checks, and effects
├── quests.test.ts # Quest progression and integrity
├── skillChecks.test.ts # Exploration/dialogue checks
├── reputation.test.ts # Alignment, reputation, integrations, and migration policy
├── achievements.test.ts # Definitions, progress, hooks, titles, and migration policy
├── cutscenes.test.ts # Cutscene data, triggers, queue, and director lifecycle
├── accessibility.test.ts # Shared preference normalization, migration, and persistence
├── input.test.ts # Semantic mappings, contexts, repeats, cleanup, and suppression
├── tutorial.test.ts # Tutorial content, completion, and unlocked Tips
├── gathering.test.ts # Gathering data, state machines, rewards, and save rules
├── crafting.test.ts # Recipes, transactions, discovery, economy, and save rules
├── cutsceneSceneTransition.test.ts # Generic Cutscene scene contracts
├── defeatSceneTransition.test.ts # Defeat result continuation contract
├── save.test.ts # Persistence and migration
└── data.test.ts # Data validation
Quest tests must cover duplicate matching monsters in one group victory, durable boss reconciliation, optional-objective reward cutoffs, hard-gate softlock safety, reward idempotency, and flat schema-v4 to nested schema-v5 migration. Map main-quest NPC objectives to live city IDs and require exact coverage of all 12 cities with stable stage/objective identities.
Tutorial tests must keep Phaser out of pure logic. Assert stable unique IDs,
valid semantic control references, idempotent completion, safe malformed-state
normalization, and exact progression-aware tip unlock sets. The committed
Playwright flow owns automatic presentation, pointer navigation, persisted
completion, F1, and Esc-menu access.
resolveSkillCheck(); do not depend on
randomness.CutsceneScene transition payload tests separate from the
Playwright visual/input flow.BattleActionSource/PartyCombatant adapters and verify HP/MP
writes reach the underlying companion state.2dnd_inventory_prefs separately from 2dnd_save.import { describe, it, expect } from "vitest";
import { rollD20, rollDice, abilityModifier } from "../src/systems/dice";
describe("rollD20 distribution", () => {
it("returns values between 1 and 20", () => {
const rolls = Array.from({ length: 200 }, () => rollD20());
expect(Math.min(...rolls)).toBeGreaterThanOrEqual(1);
expect(Math.max(...rolls)).toBeLessThanOrEqual(20);
});
it("produces roughly uniform distribution", () => {
const rolls = Array.from({ length: 10000 }, () => rollD20());
const counts = rolls.reduce((acc, roll) => {
acc[roll] = (acc[roll] || 0) + 1;
return acc;
}, {} as Record<number, number>);
.(counts).( {
(count).();
(count).();
});
});
});
describe("abilityModifier", () => {
it("calculates correct modifiers for standard scores", () => {
expect(abilityModifier(8)).toBe(-1);
expect(abilityModifier(10)).toBe(0);
expect(abilityModifier(11)).toBe(0);
expect(abilityModifier(12)).toBe(1);
expect(abilityModifier(16)).toBe(3);
expect(abilityModifier(20)).toBe(5);
});
it("handles edge cases", () => {
expect(abilityModifier(1)).toBe(-5);
expect(abilityModifier(3)).toBe(-4);
expect(abilityModifier()).();
});
});
import { calculateAttackRoll, resolveAttack } from "../src/systems/combat";
describe("attack mechanics", () => {
it("hits when roll meets or exceeds AC", () => {
const attacker = createTestPlayer({ strength: 16 }); // +3 mod
const target = createTestMonster({ ac: 13 });
// Mock d20 roll of 10
const mockRoll = 10;
const attackBonus = abilityModifier(attacker.stats.strength);
const total = mockRoll + attackBonus; // 13
expect(total).toBeGreaterThanOrEqual(target.ac);
});
it("applies critical hit on natural 20", () => {
const attacker = createTestPlayer();
const target = createTestMonster({ hp: 50 });
const normalDamage = resolveDamage(attacker, false);
const criticalDamage = resolveDamage(attacker, true);
// Critical should deal more damage
expect(criticalDamage).(normalDamage);
});
});
describe("damage calculation", () => {
it("adds ability modifier to weapon damage", () => {
const attacker = createTestPlayer({
strength: 18, // +4 modifier
equipment: { weapon: { damage: "1d8" } }
});
// Test multiple rolls
const damages = Array.from({ length: 100 }, () =>
calculateDamage(attacker)
);
// Minimum: 1 (min roll) + 4 (modifier) = 5
// Maximum: 8 (max roll) + 4 (modifier) = 12
expect(Math.min(...damages)).toBe(5);
expect(Math.max(...damages)).toBe(12);
});
});
import { gainXP, levelUp, calculateMaxHP } from "../src/systems/player";
describe("player leveling", () => {
it("levels up when reaching XP threshold", () => {
const player = createTestPlayer({ level: 1, xp: 0 });
const xpNeeded = getXPForLevel(2);
gainXP(player, xpNeeded);
expect(player.level).toBe(2);
expect(player.xp).toBe(0); // XP resets after level
});
it("increases max HP on level up", () => {
const player = createTestPlayer({
level: 1,
maxHp: 10,
stats: { constitution: 14 } // +2 modifier
});
const oldMaxHP = player.maxHp;
levelUp(player);
expect(player.maxHp).toBeGreaterThan(oldMaxHP);
// Should increase by at least 1 (minimum HP gain)
(player. - oldMaxHP).();
});
(, {
player = ({ : , : });
(player.)..();
player. = ;
(player);
(player.).();
});
});
describe("player inventory", () => {
it("equips weapon and updates attack damage", () => {
const player = createTestPlayer();
const sword = { id: "longsword", damage: "1d8", bonus: 1 };
equipWeapon(player, sword);
expect(player.equipment.weapon).toBe(sword);
expect(player.attackDamage).toContain("1d8");
});
it("prevents purchasing when insufficient gold", () => {
const player = createTestPlayer({ gold: 10 });
const expensiveItem = { id: "plate", price: 1500 };
const result = purchaseItem(player, expensiveItem);
expect(result).toBe(false);
expect(player.gold).toBe(10); // Unchanged
});
});
import { MONSTERS, ENCOUNTER_TABLES } from "../src/data/monsters";
describe("monster data integrity", () => {
it("all monsters have required fields", () => {
Object.values(MONSTERS).forEach(monster => {
expect(monster.id).toBeDefined();
expect(monster.name).toBeDefined();
expect(monster.level).toBeGreaterThan(0);
expect(monster.hp).toBeGreaterThan(0);
expect(monster.ac).toBeGreaterThan(0);
expect(monster.xp).toBeGreaterThan(0);
});
});
it("all encounter tables reference valid monsters", () => {
Object.values(ENCOUNTER_TABLES).forEach(table => {
table.forEach( {
([entry.]).();
});
});
});
(, {
.().( {
(id).();
(id)..();
(id)..();
});
});
});
import { SPELLS } from "../src/data/spells";
describe("spell data integrity", () => {
it("healing spells have healing field", () => {
Object.values(SPELLS).forEach(spell => {
if (spell.damage) {
expect(spell.healing).toBeUndefined();
}
if (spell.healing) {
expect(spell.damage).toBeUndefined();
}
});
});
it("all spells have valid MP costs", () => {
Object.values(SPELLS).forEach(spell => {
expect(spell.mpCost).toBeGreaterThan(0);
expect(spell.mpCost).toBeLessThan(50);
});
});
it("spell unlock levels are reasonable", () => {
Object.values(SPELLS).( {
(spell.).();
(spell.).();
});
});
});
// createPlayer now requires baseStats — never called without them
const defaultStats: PlayerStats = {
strength: 10, dexterity: 10, constitution: 10,
intelligence: 10, wisdom: 10, charisma: 10,
};
function createTestPlayer(overrides?: Partial<PlayerState>): PlayerState {
const player = createPlayer("Test", {
strength: 10, dexterity: 8, constitution: 12,
intelligence: 8, wisdom: 8, charisma: 8,
});
// Pin stats for deterministic tests
player.stats = {
strength: 12, dexterity: 10, constitution: 14,
intelligence: 10, wisdom: 10, charisma: 8,
};
player.maxHp = 30; player. = ;
player. = ; player. = ;
(overrides) .(player, overrides);
player;
}
import { calculatePointsSpent, isValidPointBuy, POINT_BUY_TOTAL } from "../src/systems/player";
// Standard array costs exactly 27
expect(calculatePointsSpent({ strength: 15, dexterity: 14, constitution: 13, intelligence: 12, wisdom: 10, charisma: 8 })).toBe(27);
// All 8s costs 0
expect(calculatePointsSpent({ strength: 8, dexterity: 8, constitution: 8, intelligence: 8, wisdom: 8, charisma: 8 })).toBe(0);
// Validates correctly
expect(isValidPointBuy({ strength: 15, dexterity: 14, constitution: 13, intelligence: 12, wisdom: 10, charisma: 8 })).toBe();
(({ : , : , : , : , : , : })).();
export function createTestMonster(overrides?: Partial): MonsterInstance { return { id: "testMonster", name: "Test Monster", level: 1, hp: 10, maxHp: 10, ac: 10, attack: 2, damage: "1d6", xp: 50, gold: 10, ...overrides, }; }
## Running Tests
### Command Line
```bash
# Run all tests once
npm test
# Install Chromium once, then run the campaign browser suite
npm run test:browser:install
npm run test:browser
# Watch mode (re-run on changes)
npm run test:watch
# Type checking
npm run typecheck
# Run specific test file
npx vitest run tests/dice.test.ts
playwright.config.ts starts Vite on an unused strict localhost port./2dnd/; set PLAYWRIGHT_BASE_PATH=/ to reproduce
the root-base development path.#debug-state, #debug-log, and persisted save state as authoritative
synchronization surfaces. Canvas text is not DOM text.keyboard.down(), wait across frames, then
keyboard.up(). Do not use instantaneous presses for Overworld or Ending
actions.pageerror and
console.error remain empty./defeat debug path.# Generate coverage report
npx vitest run --coverage
❌ Testing Phaser objects directly (use pure functions) ❌ Relying on random values without bounds checking ❌ Not testing edge cases (0, negative, null) ❌ Tests that depend on execution order ❌ Overly complex test setup ❌ Not cleaning up state between tests
tests/*.test.tsvitest.config.tstsconfig.json