Add state module with combat init, draw, play, end turn

This commit is contained in:
Jared Miller 2026-02-23 17:34:49 -05:00
parent 08214fc8cb
commit a1f242d54e
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
4 changed files with 188 additions and 0 deletions

3
src/effects.js vendored Normal file
View file

@ -0,0 +1,3 @@
export function resolveEffects(state) {
return state;
}

5
src/enemies.js Normal file
View file

@ -0,0 +1,5 @@
import enemyDb from "../data/enemies.json";
export function getEnemy(id) {
return enemyDb[id];
}

106
src/state.js Normal file
View file

@ -0,0 +1,106 @@
import { getCard, getStarterDeck } from "./cards.js";
import { resolveEffects } from "./effects.js";
import { getEnemy } from "./enemies.js";
export function createCombatState(character, enemyId) {
const enemy = getEnemy(enemyId);
return {
player: {
hp: 11,
maxHp: 11,
energy: 3,
maxEnergy: 3,
block: 0,
strength: 0,
vulnerable: 0,
weak: 0,
drawPile: shuffle([...getStarterDeck(character)]),
hand: [],
discardPile: [],
exhaustPile: [],
powers: [],
},
enemy: {
id: enemy.id,
name: enemy.name,
hp: enemy.hp,
maxHp: enemy.hp,
block: 0,
strength: 0,
vulnerable: 0,
weak: 0,
actionType: enemy.actionType,
actions: enemy.actions,
actionTrack: enemy.actionTrack || null,
trackPosition: 0,
},
combat: {
turn: 1,
phase: "player_turn",
dieResult: null,
selectedCard: null,
log: [],
},
};
}
export function drawCards(state, count) {
let drawPile = [...state.player.drawPile];
let discardPile = [...state.player.discardPile];
const hand = [...state.player.hand];
for (let i = 0; i < count; i++) {
if (drawPile.length === 0) {
drawPile = shuffle(discardPile);
discardPile = [];
}
if (drawPile.length > 0) {
hand.push(drawPile.pop());
}
}
return {
...state,
player: { ...state.player, drawPile, hand, discardPile },
};
}
export function playCard(state, handIndex) {
const cardId = state.player.hand[handIndex];
const card = getCard(cardId);
if (state.player.energy < card.cost) return null;
const hand = [...state.player.hand];
hand.splice(handIndex, 1);
const discardPile = [...state.player.discardPile, cardId];
const energy = state.player.energy - card.cost;
let next = {
...state,
player: { ...state.player, hand, discardPile, energy },
};
next = resolveEffects(next, card.effects, "player", "enemy");
return next;
}
export function endTurn(state) {
return {
...state,
player: {
...state.player,
hand: [],
discardPile: [...state.player.discardPile, ...state.player.hand],
},
combat: { ...state.combat, phase: "enemy_turn" },
};
}
function shuffle(arr) {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}

74
src/state.test.js Normal file
View file

@ -0,0 +1,74 @@
import { describe, expect, test } from "bun:test";
import { createCombatState, drawCards, endTurn, playCard } from "./state.js";
describe("createCombatState", () => {
test("creates initial state with shuffled deck and correct values", () => {
const state = createCombatState("ironclad", "jaw_worm");
expect(state.player.hp).toBe(11);
expect(state.player.maxHp).toBe(11);
expect(state.player.energy).toBe(3);
expect(state.player.block).toBe(0);
expect(state.player.strength).toBe(0);
expect(state.player.hand).toHaveLength(0);
expect(state.player.drawPile).toHaveLength(10);
expect(state.enemy.name).toBe("Jaw Worm");
expect(state.enemy.hp).toBeGreaterThan(0);
expect(state.combat.turn).toBe(1);
expect(state.combat.phase).toBe("player_turn");
});
});
describe("drawCards", () => {
test("moves cards from draw pile to hand", () => {
const state = createCombatState("ironclad", "jaw_worm");
const next = drawCards(state, 5);
expect(next.player.hand).toHaveLength(5);
expect(next.player.drawPile).toHaveLength(5);
});
test("shuffles discard into draw when draw pile runs out", () => {
let state = createCombatState("ironclad", "jaw_worm");
state = {
...state,
player: {
...state.player,
drawPile: state.player.drawPile.slice(0, 2),
discardPile: state.player.drawPile.slice(2),
},
};
const next = drawCards(state, 5);
expect(next.player.hand).toHaveLength(5);
expect(next.player.discardPile).toHaveLength(0);
});
});
describe("playCard", () => {
test("deducts energy and moves card to discard", () => {
let state = createCombatState("ironclad", "jaw_worm");
state = drawCards(state, 5);
const cardIndex = state.player.hand.indexOf("strike_r");
const next = playCard(state, cardIndex);
expect(next.player.energy).toBe(2);
expect(next.player.hand).toHaveLength(4);
});
test("returns null if not enough energy", () => {
let state = createCombatState("ironclad", "jaw_worm");
state = drawCards(state, 5);
state = { ...state, player: { ...state.player, energy: 0 } };
const cardIndex = state.player.hand.indexOf("strike_r");
const result = playCard(state, cardIndex);
expect(result).toBeNull();
});
});
describe("endTurn", () => {
test("discards hand and switches to enemy phase", () => {
let state = createCombatState("ironclad", "jaw_worm");
state = drawCards(state, 5);
const next = endTurn(state);
expect(next.player.hand).toHaveLength(0);
expect(next.player.discardPile.length).toBeGreaterThan(0);
expect(next.combat.phase).toBe("enemy_turn");
});
});