From 30e3f88b219f4d683263702f1f1b1993ef1b2b40 Mon Sep 17 00:00:00 2001 From: Tim Bendt Date: Fri, 10 Apr 2026 13:36:43 -0400 Subject: [PATCH] Refine setup and draft interactions --- src/main.ts | 333 ++++++++++++++++++++-------- src/rules-initiative.ts | 8 +- src/rules-scoring.ts | 7 - src/rules-weather.ts | 36 +++- src/state.ts | 22 +- src/styles.css | 466 +++++++++++++++++++++++++++++++++++----- src/types.ts | 13 +- 7 files changed, 718 insertions(+), 167 deletions(-) diff --git a/src/main.ts b/src/main.ts index f24dfdd..3ff357d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,13 +24,15 @@ import { } from "./rules-scoring"; import { createInitiativeDraft, + getInitiativeGraceRounds, } from "./rules-initiative"; import { - WEATHER_CARDS, + WEATHER_OFFER_PAIRS, createWeatherDraft, getCurrentWeatherPlayerId, getWeatherCard, isWeatherCardAvailable, + isWeatherOfferResolved, } from "./rules-weather"; import { createInitialState, @@ -38,6 +40,7 @@ import { createRandomizedSeedInputs, createSetupState, getMaxStartingNodesPerPlayer, + normalizeSeedInputs, } from "./state"; import type { GameState, @@ -64,10 +67,12 @@ let state: GameState = createInitialState(setup); let isNewGameModalOpen = false; let previousScoreSnapshot: ScoreSnapshot[] | null = null; let setupTab: "board" | "rules" | "events" | "players" = "board"; +let draggedSetupSeed: { playerId: number; seedIndex: number } | null = null; function rebuildSetup(overrides: Partial = {}) { setup = createSetupState( overrides.playerCount ?? setup.playerCount, + overrides.playerNames ?? setup.playerNames, overrides.columns ?? setup.columns, overrides.rows ?? setup.rows, overrides.startingNodesPerPlayer ?? setup.startingNodesPerPlayer, @@ -89,6 +94,10 @@ function getLiveExposureScores() { return buildEnergySimulation(state).scores; } +function getProjectedIncomeScores() { + return getLiveExposureScores().map((score) => score + 1); +} + function getTopLeafCount() { const childrenMap = buildChildrenMap(); let count = 0; @@ -120,9 +129,9 @@ function getWinConditionSummary() { } function getScoreSnapshot() { - const exposureScores = getLiveExposureScores(); + const projectedIncomeScores = getProjectedIncomeScores(); return state.players.map((player, index) => ({ - currentExposure: exposureScores[index], + projectedIncome: projectedIncomeScores[index], growthPoints: player.growthPoints, bankedPoints: player.bankedPoints, lifetimeGrowthIncome: player.lifetimeGrowthIncome, @@ -133,6 +142,10 @@ function getCurrentPlayer() { return state.players[state.activePlayerId]; } +function getPlayerById(playerId: number) { + return state.players.find((player) => player.id === playerId) ?? null; +} + function getOrderedPlayers(playerIds: number[]) { return playerIds.map((playerId) => state.players[playerId]); } @@ -170,6 +183,17 @@ function getCurrentBiddingPlayer() { return state.players[state.initiativeDraft.biddingOrder[state.initiativeDraft.biddingIndex]]; } +function getInitiativeBonusStatus() { + const graceRounds = getInitiativeGraceRounds(state); + const roundsRemaining = Math.max(0, Math.ceil(graceRounds - (state.round - 1))); + const bonusActive = roundsRemaining === 0; + + return { + bonusActive, + roundsRemaining, + }; +} + function getCurrentWeatherDraftPlayer() { if (!state.weatherDraft) { return null; @@ -212,6 +236,13 @@ function getLegalMovesForSource(sourceKey: NodeKey, player: Player) { return moves; } + const freeVerticalMovesUsed = state.turnMoves.filter((move) => move.type === "grow" && move.cost === 0).length; + const freeVerticalMovesRemaining = Math.max(0, 3 - freeVerticalMovesUsed); + + if (freeVerticalMovesRemaining <= 0) { + return moves; + } + return moves.map((move) => move.direction === "vertical" ? { ...move, cost: Math.max(0, move.cost - 1) } : move); @@ -409,15 +440,17 @@ function finalizeInitiativeDraft() { nextTurnOrder.forEach((playerId, seatIndex) => { const bonus = draft.seatBonuses[seatIndex] ?? 0; - if (bonus > 0) { - awardGrowth(state.players[playerId], bonus); + const player = getPlayerById(playerId); + if (bonus > 0 && player) { + awardGrowth(player, bonus); } }); const seatSummary = nextTurnOrder .map((playerId, seatIndex) => { const bonus = draft.seatBonuses[seatIndex] ?? 0; - return `${state.players[playerId].name}: Seat ${seatIndex + 1}${bonus > 0 ? ` (+${bonus})` : ""}`; + const player = getPlayerById(playerId); + return `${player?.name ?? `Player ${playerId + 1}`}: Seat ${seatIndex + 1}${bonus > 0 ? ` (+${bonus})` : ""}`; }) .join(" | "); @@ -434,7 +467,7 @@ function chooseInitiativeSeat(seatIndex: number) { const playerId = draft.biddingOrder[draft.biddingIndex]; draft.seatAssignments[seatIndex] = playerId; - state.history.unshift(`${state.players[playerId].name} claimed seat ${seatIndex + 1} for round ${state.round}.`); + state.history.unshift(`${getPlayerById(playerId)?.name ?? `Player ${playerId + 1}`} claimed seat ${seatIndex + 1} for round ${state.round}.`); if (draft.biddingIndex >= draft.biddingOrder.length - 1) { finalizeInitiativeDraft(); @@ -481,18 +514,19 @@ function finalizeWeatherDraft() { moveToFirstPlayableTurn(); } -function chooseWeatherAction(cardId: WeatherCardId, action: "draft" | "ban") { +function chooseWeatherAction(offerId: string, cardId: WeatherCardId, action: "draft" | "ban") { const draft = state.weatherDraft; - if (!draft || !isWeatherCardAvailable(draft, cardId)) { + if (!draft || !isWeatherCardAvailable(draft, offerId, cardId)) { return; } const playerId = getCurrentWeatherPlayerId(draft); - const card = getWeatherCard(cardId); if (action === "draft") { + const card = getWeatherCard(cardId); draft.drafted.push(cardId); state.history.unshift(`${state.players[playerId].name} drafted ${card?.title ?? cardId}.`); } else { + const card = getWeatherCard(cardId); draft.banned.push(cardId); state.history.unshift(`${state.players[playerId].name} banned ${card?.title ?? cardId}.`); } @@ -790,6 +824,45 @@ function moveSetupPlayer(fromIndex: number, toIndex: number) { [setup.paletteOrder[fromIndex], setup.paletteOrder[toIndex]] = [setup.paletteOrder[toIndex], setup.paletteOrder[fromIndex]]; [setup.seedInputs[fromIndex], setup.seedInputs[toIndex]] = [setup.seedInputs[toIndex], setup.seedInputs[fromIndex]]; + [setup.playerNames[fromIndex], setup.playerNames[toIndex]] = [setup.playerNames[toIndex], setup.playerNames[fromIndex]]; + render(); +} + +function getSetupSeedColumns() { + return normalizeSeedInputs(setup).map((columns) => [...columns]); +} + +function setSetupSeedColumns(seedColumnsByPlayer: number[][]) { + setup.seedInputs = seedColumnsByPlayer.map((columns) => columns.join(", ") ? columns.map((column) => String(column + 1)).join(", ") : ""); +} + +function moveSetupSeed(playerId: number, seedIndex: number, targetColumn: number) { + const seedColumnsByPlayer = getSetupSeedColumns(); + const originColumn = seedColumnsByPlayer[playerId]?.[seedIndex]; + if (originColumn === undefined || originColumn === targetColumn) { + return; + } + + let swapped = false; + seedColumnsByPlayer.forEach((columns, otherPlayerId) => { + columns.forEach((column, otherSeedIndex) => { + if (otherPlayerId === playerId && otherSeedIndex === seedIndex) { + return; + } + + if (column === targetColumn) { + seedColumnsByPlayer[otherPlayerId][otherSeedIndex] = originColumn; + swapped = true; + } + }); + }); + + seedColumnsByPlayer[playerId][seedIndex] = targetColumn; + if (!swapped) { + seedColumnsByPlayer[playerId] = [...seedColumnsByPlayer[playerId]]; + } + + setSetupSeedColumns(seedColumnsByPlayer); render(); } @@ -809,8 +882,15 @@ function renderNewGameModal() { } const maxSeeds = getMaxStartingNodesPerPlayer(setup.playerCount, setup.columns); - const previewPlayers = createPlayers(setup.playerCount, setup.paletteOrder); - const draftCountMax = WEATHER_CARDS.length; + const previewPlayers = createPlayers(setup.playerCount, setup.paletteOrder, setup.playerNames); + const draftCountMax = WEATHER_OFFER_PAIRS.length; + const previewSeedColumns = getSetupSeedColumns(); + const seedMarkers = previewSeedColumns.flatMap((columns, playerId) => columns.map((column, seedIndex) => ({ + playerId, + seedIndex, + column, + player: previewPlayers[playerId], + }))); return ` -