Compare commits
2 Commits
8b50482621
...
e11264168c
| Author | SHA1 | Date | |
|---|---|---|---|
| e11264168c | |||
| 1cc85397bd |
104
src/main.ts
104
src/main.ts
@@ -1,7 +1,5 @@
|
||||
import "./styles.css";
|
||||
|
||||
console.log("[DEBUG] Starting main.ts import...");
|
||||
|
||||
import {
|
||||
ROOT_SHIFT_COST,
|
||||
ROUND_ANIMATION_BONUS_MS,
|
||||
@@ -28,6 +26,7 @@ import {
|
||||
createInitiativeDraft,
|
||||
} from "./rules-initiative";
|
||||
import {
|
||||
WEATHER_CARDS,
|
||||
createWeatherDraft,
|
||||
getCurrentWeatherPlayerId,
|
||||
getWeatherCard,
|
||||
@@ -54,22 +53,17 @@ import type {
|
||||
import { keyFor, parseKey, tint, wait } from "./utils";
|
||||
|
||||
const app = document.querySelector("#app");
|
||||
console.log("[DEBUG] App element found:", app);
|
||||
|
||||
if (!(app instanceof HTMLElement)) {
|
||||
console.error("[DEBUG] #app container not found or not HTMLElement");
|
||||
throw new Error("#app container not found");
|
||||
}
|
||||
|
||||
console.log("[DEBUG] Initializing state...");
|
||||
let roundAnimationToken = 0;
|
||||
let setup: SetupState = createSetupState();
|
||||
console.log("[DEBUG] Setup created:", setup);
|
||||
let state: GameState = createInitialState(setup);
|
||||
console.log("[DEBUG] Initial state created:", state);
|
||||
let isNewGameModalOpen = false;
|
||||
let previousScoreSnapshot: ScoreSnapshot[] | null = null;
|
||||
console.log("[DEBUG] State initialized, proceeding to render...");
|
||||
let setupTab: "board" | "rules" | "events" | "players" = "board";
|
||||
|
||||
function rebuildSetup(overrides: Partial<SetupState> = {}) {
|
||||
setup = createSetupState(
|
||||
@@ -84,6 +78,7 @@ function rebuildSetup(overrides: Partial<SetupState> = {}) {
|
||||
overrides.initiativeMode ?? setup.initiativeMode,
|
||||
overrides.biddingOrderRule ?? setup.biddingOrderRule,
|
||||
overrides.weatherDraftEnabled ?? setup.weatherDraftEnabled,
|
||||
overrides.weatherDraftCount ?? setup.weatherDraftCount,
|
||||
overrides.winCondition ?? setup.winCondition,
|
||||
overrides.maxRounds ?? setup.maxRounds,
|
||||
overrides.topLeafTarget ?? setup.topLeafTarget,
|
||||
@@ -154,6 +149,10 @@ function getTurnLabel() {
|
||||
return state.gameOver ? "Game Over" : `${getCurrentPlayer().name}'s turn`;
|
||||
}
|
||||
|
||||
function isBankingEnabled() {
|
||||
return state.activeRoundEffects.includes("storehouse");
|
||||
}
|
||||
|
||||
function awardGrowth(player: Player, amount: number) {
|
||||
if (amount <= 0) {
|
||||
return;
|
||||
@@ -207,7 +206,15 @@ function getSelectedRootShiftMoves() {
|
||||
}
|
||||
|
||||
function getLegalMovesForSource(sourceKey: NodeKey, player: Player) {
|
||||
return getLegalMovesForSourceForState(state, sourceKey, player);
|
||||
const moves = getLegalMovesForSourceForState(state, sourceKey, player);
|
||||
|
||||
if (!state.activeRoundEffects.includes("sun_ladder")) {
|
||||
return moves;
|
||||
}
|
||||
|
||||
return moves.map((move) => move.direction === "vertical"
|
||||
? { ...move, cost: Math.max(0, move.cost - 1) }
|
||||
: move);
|
||||
}
|
||||
|
||||
function playerHasLegalMove(player: Player) {
|
||||
@@ -621,6 +628,10 @@ function endTurn() {
|
||||
}
|
||||
|
||||
function bankGrowthAndEndTurn() {
|
||||
if (!isBankingEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const player = getCurrentPlayer();
|
||||
if (player.growthPoints <= 0) {
|
||||
endTurn();
|
||||
@@ -787,6 +798,11 @@ function randomizeStartingLocations() {
|
||||
render();
|
||||
}
|
||||
|
||||
function setSetupTab(tab: typeof setupTab) {
|
||||
setupTab = tab;
|
||||
render();
|
||||
}
|
||||
|
||||
function renderNewGameModal() {
|
||||
if (!isNewGameModalOpen) {
|
||||
return "";
|
||||
@@ -794,6 +810,7 @@ function renderNewGameModal() {
|
||||
|
||||
const maxSeeds = getMaxStartingNodesPerPlayer(setup.playerCount, setup.columns);
|
||||
const previewPlayers = createPlayers(setup.playerCount, setup.paletteOrder);
|
||||
const draftCountMax = WEATHER_CARDS.length;
|
||||
|
||||
return `
|
||||
<div class="modal-backdrop" id="new-game-modal-backdrop">
|
||||
@@ -805,9 +822,15 @@ function renderNewGameModal() {
|
||||
</div>
|
||||
<button class="ghost-button" id="close-new-game">Close</button>
|
||||
</header>
|
||||
<nav class="setup-tabs" aria-label="Setup categories">
|
||||
<button class="setup-tab${setupTab === "board" ? " setup-tab--active" : ""}" data-setup-tab="board"><span aria-hidden="true">▦</span><span>Board</span></button>
|
||||
<button class="setup-tab${setupTab === "rules" ? " setup-tab--active" : ""}" data-setup-tab="rules"><span aria-hidden="true">⚖</span><span>Rules</span></button>
|
||||
<button class="setup-tab${setupTab === "events" ? " setup-tab--active" : ""}" data-setup-tab="events"><span aria-hidden="true">☀</span><span>Events</span></button>
|
||||
<button class="setup-tab${setupTab === "players" ? " setup-tab--active" : ""}" data-setup-tab="players"><span aria-hidden="true">◉</span><span>Players</span></button>
|
||||
</nav>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- Board Settings Section -->
|
||||
${setupTab === "board" ? `
|
||||
<section class="setup-section">
|
||||
<h2 class="setup-section__title">Board Settings</h2>
|
||||
<div class="setup-grid">
|
||||
@@ -832,8 +855,9 @@ function renderNewGameModal() {
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
` : ""}
|
||||
|
||||
<!-- Game Rules Section -->
|
||||
${setupTab === "rules" ? `
|
||||
<section class="setup-section">
|
||||
<h2 class="setup-section__title">Game Rules</h2>
|
||||
<div class="setup-grid setup-grid--2col">
|
||||
@@ -871,10 +895,21 @@ function renderNewGameModal() {
|
||||
</select>
|
||||
</label>
|
||||
` : ""}
|
||||
<label class="setup-field setup-field--checkbox">
|
||||
<span class="setup-field__label">Weather Draft</span>
|
||||
<input id="weather-draft-toggle" type="checkbox" ${setup.weatherDraftEnabled ? "checked" : ""} />
|
||||
</label>
|
||||
${setup.weatherDraftEnabled ? `
|
||||
<label class="setup-field">
|
||||
<span class="setup-field__label">Weather cards in draft</span>
|
||||
<input id="weather-draft-count" type="number" min="1" max="${draftCountMax}" step="1" value="${setup.weatherDraftCount}" />
|
||||
</label>
|
||||
` : ""}
|
||||
</div>
|
||||
</section>
|
||||
` : ""}
|
||||
|
||||
<!-- Random Events Section -->
|
||||
${setupTab === "events" ? `
|
||||
<section class="setup-section">
|
||||
<h2 class="setup-section__title">Random Events</h2>
|
||||
<div class="setup-grid setup-grid--3col">
|
||||
@@ -886,14 +921,11 @@ function renderNewGameModal() {
|
||||
<span class="setup-field__label">Disease %</span>
|
||||
<input id="disease-chance" type="number" min="0" max="100" step="5" value="${setup.diseaseChance}" />
|
||||
</label>
|
||||
<label class="setup-field setup-field--checkbox">
|
||||
<span class="setup-field__label">Weather Draft</span>
|
||||
<input id="weather-draft-toggle" type="checkbox" ${setup.weatherDraftEnabled ? "checked" : ""} />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
` : ""}
|
||||
|
||||
<!-- Players Section -->
|
||||
${setupTab === "players" ? `
|
||||
<section class="setup-section">
|
||||
<h2 class="setup-section__title">Players & Colors</h2>
|
||||
<p class="setup-section__help">Reorder players to set turn order. Starting positions are auto-assigned.</p>
|
||||
@@ -912,6 +944,20 @@ function renderNewGameModal() {
|
||||
`).join("")}
|
||||
</div>
|
||||
</section>
|
||||
<section class="setup-section">
|
||||
<h2 class="setup-section__title">Starting columns</h2>
|
||||
<p class="setup-section__help">Use 1-based column numbers. Duplicate or invalid picks are auto-corrected.</p>
|
||||
<button class="ghost-button randomize-button" id="randomize-starting-locations">Randomize Starting Locations</button>
|
||||
<div class="player-list">
|
||||
${previewPlayers.map((currentPlayer, index) => `
|
||||
<label class="setup-field">
|
||||
<span class="setup-field__label" style="color: ${currentPlayer.color};">${currentPlayer.name}</span>
|
||||
<input class="seed-input" data-player-id="${index}" type="text" value="${setup.seedInputs[index] ?? ""}" placeholder="e.g. 2, 5" />
|
||||
</label>
|
||||
`).join("")}
|
||||
</div>
|
||||
</section>
|
||||
` : ""}
|
||||
</div>
|
||||
|
||||
<footer class="modal-footer">
|
||||
@@ -1259,7 +1305,7 @@ function renderSidebar() {
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button id="end-turn" ${boardLocked ? "disabled" : ""}>End Turn</button>
|
||||
<button id="bank-turn" class="ghost-button" ${boardLocked ? "disabled" : ""}>Bank Remaining</button>
|
||||
<button id="bank-turn" class="ghost-button" ${boardLocked || !isBankingEnabled() ? "disabled" : ""}>Bank Remaining</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1386,6 +1432,11 @@ function attachEvents() {
|
||||
});
|
||||
document.querySelector<HTMLInputElement>("#weather-draft-toggle")?.addEventListener("change", (event) => {
|
||||
setup.weatherDraftEnabled = (event.currentTarget as HTMLInputElement).checked;
|
||||
render();
|
||||
});
|
||||
document.querySelector<HTMLInputElement>("#weather-draft-count")?.addEventListener("change", (event) => {
|
||||
rebuildSetup({ weatherDraftCount: Math.max(1, Number((event.currentTarget as HTMLInputElement).value) || 1) });
|
||||
render();
|
||||
});
|
||||
document.querySelector<HTMLSelectElement>("#win-condition")?.addEventListener("change", (event) => {
|
||||
setup.winCondition = (event.currentTarget as HTMLSelectElement).value as SetupState["winCondition"];
|
||||
@@ -1427,13 +1478,15 @@ function attachEvents() {
|
||||
chooseWeatherAction(button.dataset.weatherCard as WeatherCardId, button.dataset.weatherAction as "draft" | "ban");
|
||||
});
|
||||
});
|
||||
document.querySelectorAll<HTMLElement>("[data-setup-tab]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
setSetupTab(button.dataset.setupTab as typeof setupTab);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function render() {
|
||||
console.log("[DEBUG] render() called, playerCount:", state?.players?.length);
|
||||
try {
|
||||
const playerCount = state.players.length;
|
||||
console.log("[DEBUG] Building HTML...");
|
||||
app.innerHTML = `
|
||||
<main class="layout" style="--player-count: ${playerCount}">
|
||||
<section class="game-area">
|
||||
@@ -1448,17 +1501,8 @@ function render() {
|
||||
${renderInitiativeModal()}
|
||||
${renderWeatherDraftModal()}
|
||||
`;
|
||||
console.log("[DEBUG] HTML rendered, attaching events...");
|
||||
attachEvents();
|
||||
console.log("[DEBUG] Events attached, getting score snapshot...");
|
||||
previousScoreSnapshot = getScoreSnapshot();
|
||||
console.log("[DEBUG] Render complete!");
|
||||
} catch (error) {
|
||||
console.error("[DEBUG] Error in render():", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[DEBUG] About to call render()...");
|
||||
render();
|
||||
console.log("[DEBUG] render() executed");
|
||||
|
||||
@@ -28,6 +28,48 @@ function getLeafCounts(state: GameState) {
|
||||
return counts;
|
||||
}
|
||||
|
||||
function getColumnPresence(state: GameState, column: number) {
|
||||
const owners = new Set<PlayerId>();
|
||||
|
||||
for (let row = 0; row < state.config.rows; row += 1) {
|
||||
const ownerId = state.nodes.get(keyFor(row, column))?.ownerId;
|
||||
if (ownerId !== undefined) {
|
||||
owners.add(ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
return [...owners];
|
||||
}
|
||||
|
||||
function applyColumnBaseEnergy(state: GameState, scores: number[], ownerId: PlayerId, playersPresent: PlayerId[]) {
|
||||
const contested = playersPresent.length > 1;
|
||||
|
||||
if (!contested) {
|
||||
scores[ownerId] += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.activeRoundEffects.includes("stalemate")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.activeRoundEffects.includes("split_light")) {
|
||||
playersPresent.forEach((playerId) => {
|
||||
scores[playerId] += 0.5;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.activeRoundEffects.includes("shared_light")) {
|
||||
playersPresent.forEach((playerId) => {
|
||||
scores[playerId] += 1;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
scores[ownerId] += 1;
|
||||
}
|
||||
|
||||
function applyWeatherEffects(state: GameState, scores: number[], energySimulation: EnergySimulation) {
|
||||
if (state.activeRoundEffects.length === 0) {
|
||||
return;
|
||||
@@ -132,6 +174,7 @@ export function buildEnergySimulation(state: GameState): EnergySimulation {
|
||||
terminalRow: state.config.rows - 1,
|
||||
intercepted: false,
|
||||
ownerId: null,
|
||||
playersPresent: [],
|
||||
hitNode: null,
|
||||
rootKey: null,
|
||||
branchNodes: [],
|
||||
@@ -142,6 +185,7 @@ export function buildEnergySimulation(state: GameState): EnergySimulation {
|
||||
|
||||
const hitNode = parseKey(hitNodeKey);
|
||||
const ownerId = state.nodes.get(hitNodeKey)?.ownerId as PlayerId;
|
||||
const playersPresent = getColumnPresence(state, column);
|
||||
const branchNodes = [hitNode];
|
||||
const branchEdges = [];
|
||||
let cursor = hitNodeKey;
|
||||
@@ -153,12 +197,13 @@ export function buildEnergySimulation(state: GameState): EnergySimulation {
|
||||
cursor = parentKey;
|
||||
}
|
||||
|
||||
scores[ownerId] += 1;
|
||||
applyColumnBaseEnergy(state, scores, ownerId, playersPresent);
|
||||
columns.push({
|
||||
column,
|
||||
terminalRow: hitNode.row,
|
||||
intercepted: true,
|
||||
ownerId,
|
||||
playersPresent,
|
||||
hitNode,
|
||||
rootKey: cursor,
|
||||
branchNodes,
|
||||
|
||||
@@ -4,12 +4,17 @@ import { shuffleArray } from "./utils";
|
||||
export const WEATHER_CARDS: WeatherCardDefinition[] = [
|
||||
{ id: "leaf_surge", title: "Leaf Surge", description: "Each leaf gives +1." },
|
||||
{ id: "branching_season", title: "Branching Season", description: "Each leaf after your first gives +1." },
|
||||
{ id: "storehouse", title: "Storehouse", description: "Banking is enabled this round." },
|
||||
{ id: "sun_ladder", title: "Sun Ladder", description: "Straight-up growth costs 0." },
|
||||
{ id: "west_light", title: "West Light", description: "Left third columns give +1." },
|
||||
{ id: "east_light", title: "East Light", description: "Right third columns give +1." },
|
||||
{ id: "high_noon", title: "High Noon", description: "Center third columns give +1." },
|
||||
{ id: "edge_bloom", title: "Edge Bloom", description: "Edge columns give +1." },
|
||||
{ id: "wide_reach", title: "Wide Reach", description: "Most columns gets +2." },
|
||||
{ id: "tall_reward", title: "Tall Reward", description: "Your tallest leaf gives +2." },
|
||||
{ id: "stalemate", title: "Stalemate", description: "Contested columns give no energy." },
|
||||
{ id: "split_light", title: "Split Light", description: "Contested columns give half to each player there." },
|
||||
{ id: "shared_light", title: "Shared Light", description: "Contested columns give full energy to each player there." },
|
||||
];
|
||||
|
||||
export const WEATHER_CARD_LOOKUP = new Map<WeatherCardId, WeatherCardDefinition>(
|
||||
@@ -17,7 +22,7 @@ export const WEATHER_CARD_LOOKUP = new Map<WeatherCardId, WeatherCardDefinition>
|
||||
);
|
||||
|
||||
export function createWeatherDraft(state: GameState): WeatherDraftState {
|
||||
const rowSize = Math.min(WEATHER_CARDS.length, state.players.length + 2);
|
||||
const rowSize = Math.min(WEATHER_CARDS.length, state.config.weatherDraftCount);
|
||||
|
||||
return {
|
||||
playerOrder: [...state.turnOrder],
|
||||
|
||||
10
src/state.ts
10
src/state.ts
@@ -79,15 +79,17 @@ export function createSetupState(
|
||||
initiativeMode: SetupState["initiativeMode"] = "fixed",
|
||||
biddingOrderRule: SetupState["biddingOrderRule"] = "rotating",
|
||||
weatherDraftEnabled = true,
|
||||
weatherDraftCount = playerCount + 2,
|
||||
winCondition: SetupState["winCondition"] = "rounds",
|
||||
maxRounds = 12,
|
||||
topLeafTarget = 4,
|
||||
): SetupState {
|
||||
console.log("[DEBUG] createSetupState started");
|
||||
const clampedSeeds = Math.min(startingNodesPerPlayer, getMaxStartingNodesPerPlayer(playerCount, columns));
|
||||
const defaults = createDefaultSeedInputs(playerCount, columns, clampedSeeds);
|
||||
const paletteDefaults = createDefaultPaletteOrder(playerCount);
|
||||
|
||||
return {
|
||||
const result = {
|
||||
playerCount,
|
||||
columns,
|
||||
rows,
|
||||
@@ -99,10 +101,13 @@ export function createSetupState(
|
||||
initiativeMode,
|
||||
biddingOrderRule,
|
||||
weatherDraftEnabled,
|
||||
weatherDraftCount: Math.max(1, weatherDraftCount),
|
||||
winCondition,
|
||||
maxRounds,
|
||||
topLeafTarget: Math.max(1, Math.min(columns, topLeafTarget)),
|
||||
};
|
||||
console.log("[DEBUG] createSetupState completed");
|
||||
return result;
|
||||
}
|
||||
|
||||
export function createPlayers(playerCount: number, paletteOrder = createDefaultPaletteOrder(playerCount)): Player[] {
|
||||
@@ -162,6 +167,7 @@ export function normalizeSeedInputs(setup: SetupState) {
|
||||
}
|
||||
|
||||
export function createInitialState(setup: SetupState): GameState {
|
||||
console.log("[DEBUG] createInitialState started");
|
||||
const playerPaletteOrder = [...setup.paletteOrder];
|
||||
const players = createPlayers(setup.playerCount, playerPaletteOrder);
|
||||
const turnOrder = players.map((player) => player.id);
|
||||
@@ -175,6 +181,7 @@ export function createInitialState(setup: SetupState): GameState {
|
||||
});
|
||||
});
|
||||
|
||||
console.log("[DEBUG] createInitialState completed");
|
||||
return {
|
||||
config: {
|
||||
columns: setup.columns,
|
||||
@@ -185,6 +192,7 @@ export function createInitialState(setup: SetupState): GameState {
|
||||
initiativeMode: setup.initiativeMode,
|
||||
biddingOrderRule: setup.biddingOrderRule,
|
||||
weatherDraftEnabled: setup.weatherDraftEnabled,
|
||||
weatherDraftCount: setup.weatherDraftCount,
|
||||
winCondition: setup.winCondition,
|
||||
maxRounds: setup.maxRounds,
|
||||
topLeafTarget: setup.topLeafTarget,
|
||||
|
||||
@@ -91,14 +91,14 @@ html, body {
|
||||
/* Board - fits within shell */
|
||||
.board {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
aspect-ratio: var(--board-columns) / var(--board-rows);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--board-columns), 1fr);
|
||||
grid-template-rows: repeat(var(--board-rows), 1fr);
|
||||
grid-template-columns: repeat(var(--board-columns), minmax(0, 1fr));
|
||||
grid-template-rows: repeat(var(--board-rows), minmax(0, 1fr));
|
||||
gap: clamp(2px, 0.3cqmin, 4px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -174,6 +174,7 @@ html, body {
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.cell__shade {
|
||||
|
||||
10
src/types.ts
10
src/types.ts
@@ -24,6 +24,7 @@ export type SetupState = {
|
||||
initiativeMode: "fixed" | "bid";
|
||||
biddingOrderRule: "rotating" | "lowest_growth_income";
|
||||
weatherDraftEnabled: boolean;
|
||||
weatherDraftCount: number;
|
||||
winCondition: "rounds" | "top_leaves";
|
||||
maxRounds: number;
|
||||
topLeafTarget: number;
|
||||
@@ -52,6 +53,7 @@ export type GameConfig = {
|
||||
initiativeMode: SetupState["initiativeMode"];
|
||||
biddingOrderRule: SetupState["biddingOrderRule"];
|
||||
weatherDraftEnabled: boolean;
|
||||
weatherDraftCount: number;
|
||||
winCondition: SetupState["winCondition"];
|
||||
maxRounds: number;
|
||||
topLeafTarget: number;
|
||||
@@ -110,6 +112,7 @@ export type ColumnEnergy = {
|
||||
terminalRow: number;
|
||||
intercepted: boolean;
|
||||
ownerId: PlayerId | null;
|
||||
playersPresent: PlayerId[];
|
||||
hitNode: Position | null;
|
||||
rootKey: NodeKey | null;
|
||||
branchNodes: Position[];
|
||||
@@ -183,12 +186,17 @@ export type GamePhase = "initiative" | "turn" | "round_end" | "game_over";
|
||||
export type WeatherCardId =
|
||||
| "leaf_surge"
|
||||
| "branching_season"
|
||||
| "storehouse"
|
||||
| "sun_ladder"
|
||||
| "west_light"
|
||||
| "east_light"
|
||||
| "high_noon"
|
||||
| "edge_bloom"
|
||||
| "wide_reach"
|
||||
| "tall_reward";
|
||||
| "tall_reward"
|
||||
| "stalemate"
|
||||
| "split_light"
|
||||
| "shared_light";
|
||||
|
||||
export type WeatherCardDefinition = {
|
||||
id: WeatherCardId;
|
||||
|
||||
Reference in New Issue
Block a user