From 2b6662c2aa1aa352cdf77e0640119fb98f419438 Mon Sep 17 00:00:00 2001 From: Thomas Riffard Date: Mon, 10 Feb 2025 09:48:31 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=86=99=20Add=20leveling=20mechanic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/api/transpose-handle.ts | 9 +++++++ core/api/transpose.test.ts | 3 ++- core/automations/go-to-next-combat-phase.ts | 2 -- core/automations/go-to-next-planning-phase.ts | 26 +++++++++++++++++++ ...game-over-with-p1-advantage-against-bot.ts | 14 +++++----- .../with-game-over-with-p1-advantage.ts | 4 +++ .../with-one-bought-hero-and-level-two.ts | 11 ++++++++ core/fixtures/with-two-bot-game-fighting.ts | 20 +++----------- core/fixtures/with-two-pieces-on-board.ts | 4 +++ core/types/display.ts | 1 + core/utils/connect-bot.ts | 16 +++++++++++- core/utils/is-bot-out-of-moves.ts | 21 +++++++++++++++ interface/utils/portray.ts | 1 + interface/utils/render-piece-highlight.ts | 8 +++++- sandbox/utils/display-factory.ts | 3 +++ 15 files changed, 113 insertions(+), 30 deletions(-) create mode 100644 core/automations/go-to-next-planning-phase.ts create mode 100644 core/fixtures/with-one-bought-hero-and-level-two.ts create mode 100644 core/utils/is-bot-out-of-moves.ts diff --git a/core/api/transpose-handle.ts b/core/api/transpose-handle.ts index 587ed1d..b6da755 100644 --- a/core/api/transpose-handle.ts +++ b/core/api/transpose-handle.ts @@ -163,6 +163,15 @@ export function transposeHandle(context: BackContext): Observable { game.playerHeroes[publicKey].push(grabbedHero); benchHeroes[grabPiece.benchPosition] = ungrabbedHero; + + if ( + game.playerHeroes[publicKey].length > + game.playerLevel[publicKey] + ) { + await abort(); + return; + } + await commit(game); return; } diff --git a/core/api/transpose.test.ts b/core/api/transpose.test.ts index 202ea38..e40024b 100644 --- a/core/api/transpose.test.ts +++ b/core/api/transpose.test.ts @@ -9,6 +9,7 @@ import { withTwoPiecesOnBoard } from "../fixtures/with-two-pieces-on-board.js"; import { getGame } from "../utils/get-game.js"; import { withTwoPlayersInCombat } from "../fixtures/with-two-players-in-combat.js"; import { asPlayerShopBuy } from "../automations/as-player-shop-buy.js"; +import { withOneBoughtHeroAndLevelTwo } from "../fixtures/with-one-bought-hero-and-level-two.js"; test("Transpose shall move piece to an empty cell", async () => { const testContext = await withTwoPlayerGameStarted(); @@ -16,7 +17,7 @@ test("Transpose shall move piece to an empty cell", async () => { }); test("Transpose from bench to board", async () => { - const testContext = await withOneBoughtHero(); + const testContext = await withOneBoughtHeroAndLevelTwo(); await asPlayerTransposeBenchToBoard(testContext); const game = await getGame(testContext); const publicKey = testContext.frontContexts[0].publicKey || "Error"; diff --git a/core/automations/go-to-next-combat-phase.ts b/core/automations/go-to-next-combat-phase.ts index 3fae1c3..22872d1 100644 --- a/core/automations/go-to-next-combat-phase.ts +++ b/core/automations/go-to-next-combat-phase.ts @@ -1,6 +1,4 @@ import type { TestContext } from "../types/test-context.js"; -import { firstValueFrom, filter } from "rxjs"; -import { getPhaseDuration } from "../utils/get-phase-duration.js"; import { getGame } from "../utils/get-game.js"; import { Phase } from "../types/phase.js"; import { goToNextPhase } from "./go-to-next-phase.js"; diff --git a/core/automations/go-to-next-planning-phase.ts b/core/automations/go-to-next-planning-phase.ts new file mode 100644 index 0000000..0e33b73 --- /dev/null +++ b/core/automations/go-to-next-planning-phase.ts @@ -0,0 +1,26 @@ +import type { TestContext } from "../types/test-context.js"; +import { getGame } from "../utils/get-game.js"; +import { Phase } from "../types/phase.js"; +import { goToNextPhase } from "./go-to-next-phase.js"; + +export async function goToNextPlanningPhase( + testContext: TestContext, + playerIndex = 0, +) { + const frontContext = testContext.frontContexts[playerIndex]; + + if (!frontContext) { + throw new Error(`Player ${playerIndex} not found !`); + } + + if (!frontContext.playsig) { + throw new Error(`Player ${playerIndex} has not initiated the game !`); + } + + let game = await getGame(testContext); + + do { + await goToNextPhase(testContext); + game = await getGame(testContext); + } while (game?.phase !== Phase.Planning); +} diff --git a/core/fixtures/with-game-over-with-p1-advantage-against-bot.ts b/core/fixtures/with-game-over-with-p1-advantage-against-bot.ts index c366d2a..5d232b2 100644 --- a/core/fixtures/with-game-over-with-p1-advantage-against-bot.ts +++ b/core/fixtures/with-game-over-with-p1-advantage-against-bot.ts @@ -1,12 +1,15 @@ import { observeGame } from "../api/observe-game.js"; import { asNewPlayerConnect } from "../automations/as-new-player-connect.js"; import { asPlayerInitiateGame } from "../automations/as-player-initiate-game.js"; +import { asPlayerLevelUp } from "../automations/as-player-level-up.js"; import { asPlayerShopBuy } from "../automations/as-player-shop-buy.js"; import { asPlayerTransposeBenchToBoard } from "../automations/as-player-transpose-bench-to-board.js"; import { goToNextPhase } from "../automations/go-to-next-phase.js"; +import { goToNextPlanningPhase } from "../automations/go-to-next-planning-phase.js"; import type { TestContext } from "../types/test-context.js"; import { connectBot } from "../utils/connect-bot.js"; import { getGame } from "../utils/get-game.js"; +import { isBotOutOfMoves } from "../utils/is-bot-out-of-moves.js"; import { isGameInProgress } from "../utils/is-game-in-progress.js"; import { withServerStarted } from "./with-server-started.js"; import { firstValueFrom, filter, timeout } from "rxjs"; @@ -27,7 +30,9 @@ export async function withGameOverWithP1AdvantageAgainstBot(): Promise { - return ( - game.playerMoney[frontContext.publicKey] < 3 && - Object.values(game.playerBenches[frontContext.publicKey]).filter( - Boolean, - ).length === 0 - ); - }), + filter((game) => isBotOutOfMoves(game, frontContext.publicKey)), timeout(1000), ), ); diff --git a/core/fixtures/with-game-over-with-p1-advantage.ts b/core/fixtures/with-game-over-with-p1-advantage.ts index e8da42d..012bedf 100644 --- a/core/fixtures/with-game-over-with-p1-advantage.ts +++ b/core/fixtures/with-game-over-with-p1-advantage.ts @@ -1,6 +1,8 @@ +import { asPlayerLevelUp } from "../automations/as-player-level-up.js"; import { asPlayerShopBuy } from "../automations/as-player-shop-buy.js"; import { asPlayerTransposeBenchToBoard } from "../automations/as-player-transpose-bench-to-board.js"; import { goToNextPhase } from "../automations/go-to-next-phase.js"; +import { goToNextPlanningPhase } from "../automations/go-to-next-planning-phase.js"; import type { TestContext } from "../types/test-context.js"; import { getGame } from "../utils/get-game.js"; import { isGameInProgress } from "../utils/is-game-in-progress.js"; @@ -9,6 +11,8 @@ import { withTwoPlayerGameStarted } from "./with-two-player-game-started.js"; export async function withGameOverWithP1Advantage(): Promise { const testContext = await withTwoPlayerGameStarted(); await asPlayerShopBuy(testContext); + await goToNextPlanningPhase(testContext); + await asPlayerLevelUp(testContext); await asPlayerTransposeBenchToBoard(testContext); while (isGameInProgress(await getGame(testContext))) { diff --git a/core/fixtures/with-one-bought-hero-and-level-two.ts b/core/fixtures/with-one-bought-hero-and-level-two.ts new file mode 100644 index 0000000..faa830d --- /dev/null +++ b/core/fixtures/with-one-bought-hero-and-level-two.ts @@ -0,0 +1,11 @@ +import { asPlayerLevelUp } from "../automations/as-player-level-up.js"; +import { goToNextPlanningPhase } from "../automations/go-to-next-planning-phase.js"; +import type { TestContext } from "../types/test-context.js"; +import { withOneBoughtHero } from "./with-one-bought-hero.js"; + +export async function withOneBoughtHeroAndLevelTwo(): Promise { + const testContext = await withOneBoughtHero(); + await goToNextPlanningPhase(testContext); + await asPlayerLevelUp(testContext); + return testContext; +} diff --git a/core/fixtures/with-two-bot-game-fighting.ts b/core/fixtures/with-two-bot-game-fighting.ts index 27ebbb1..5c1ff12 100644 --- a/core/fixtures/with-two-bot-game-fighting.ts +++ b/core/fixtures/with-two-bot-game-fighting.ts @@ -5,6 +5,7 @@ import { withTwoBotGameStarted } from "./with-two-bot-game-started.js"; import { observeGame } from "../api/observe-game.js"; import { firstValueFrom, filter, timeout } from "rxjs"; import { Phase } from "../types/phase.js"; +import { isBotOutOfMoves } from "../utils/is-bot-out-of-moves.js"; export async function withTwoBotGameFighting(): Promise { const testContext = await withTwoBotGameStarted(); @@ -19,35 +20,20 @@ export async function withTwoBotGameFighting(): Promise { while (getHeroCountA() < 5 || getHeroCountB() < 5) { await firstValueFrom( observeGame(frontContextA).pipe( - filter((game) => { - return ( - game.playerMoney[frontContextA.publicKey] < 3 && - Object.values(game.playerBenches[frontContextA.publicKey]).filter( - Boolean, - ).length === 0 - ); - }), + filter((game) => isBotOutOfMoves(game, frontContextA.publicKey)), timeout(1000), ), ); await firstValueFrom( observeGame(frontContextB).pipe( - filter((game) => { - return ( - game.playerMoney[frontContextB.publicKey] < 3 && - Object.values(game.playerBenches[frontContextB.publicKey]).filter( - Boolean, - ).length === 0 - ); - }), + filter((game) => isBotOutOfMoves(game, frontContextB.publicKey)), timeout(1000), ), ); await goToNextPhase(testContext); await goToNextPhase(testContext); - game = await getGame(testContext); } diff --git a/core/fixtures/with-two-pieces-on-board.ts b/core/fixtures/with-two-pieces-on-board.ts index a2b7ed6..4799f9a 100644 --- a/core/fixtures/with-two-pieces-on-board.ts +++ b/core/fixtures/with-two-pieces-on-board.ts @@ -1,9 +1,13 @@ import type { TestContext } from "../types/test-context.js"; import { withOneBoughtHero } from "./with-one-bought-hero.js"; import { asPlayerTransposeBenchToBoard } from "../automations/as-player-transpose-bench-to-board.js"; +import { asPlayerLevelUp } from "../automations/as-player-level-up.js"; +import { goToNextPlanningPhase } from "../automations/go-to-next-planning-phase.js"; export async function withTwoPiecesOnBoard(): Promise { const testContext = await withOneBoughtHero(); + await goToNextPlanningPhase(testContext); + await asPlayerLevelUp(testContext); await asPlayerTransposeBenchToBoard(testContext, 0, { positionX: 1, diff --git a/core/types/display.ts b/core/types/display.ts index 6138f1a..22750b3 100644 --- a/core/types/display.ts +++ b/core/types/display.ts @@ -5,6 +5,7 @@ import type { Appellation } from "./appellation.js"; export interface Display { pieces: Piece[]; + level: number; players: PlayerDisplay[]; shop: Appellation[]; bench: Record; diff --git a/core/utils/connect-bot.ts b/core/utils/connect-bot.ts index c8cdd6c..996a862 100644 --- a/core/utils/connect-bot.ts +++ b/core/utils/connect-bot.ts @@ -6,6 +6,8 @@ import { getHeroCost } from "./get-hero-cost.js"; import { shopBuy } from "../api/shop-buy.js"; import { transpose } from "../api/transpose.js"; import { debounceTime } from "rxjs/operators"; +import { getLevelUpCost } from "./get-level-up-cost.js"; +import { levelUp } from "../api/level-up.js"; export async function connectBot(frontContext: FrontContext, debounce = 0) { const initiateGameResponse = await initiateGame(frontContext); @@ -18,11 +20,23 @@ export async function connectBot(frontContext: FrontContext, debounce = 0) { if (game.phase === Phase.Planning) { const bench = game.playerBenches[frontContext.publicKey] || {}; const benchEntries = Object.entries(bench); + const levelUpCost = getLevelUpCost(game, frontContext.publicKey); + + if (game.playerMoney[frontContext.publicKey] >= levelUpCost) { + await levelUp(frontContext); + return; + } + + const board = game.playerHeroes[frontContext.publicKey]; + const boardSize = board.filter(Boolean).length; + + const availableBoardSlot = + boardSize < game.playerLevel[frontContext.publicKey]; for (const [_benchPosition, hero] of benchEntries) { const benchPosition = Number.parseInt(_benchPosition); - if (hero) { + if (hero && availableBoardSlot) { const grab = { benchPosition }; const positionX = Math.floor(Math.random() * 5); const positionY = Math.floor(Math.random() * 10); diff --git a/core/utils/is-bot-out-of-moves.ts b/core/utils/is-bot-out-of-moves.ts new file mode 100644 index 0000000..3e1adf9 --- /dev/null +++ b/core/utils/is-bot-out-of-moves.ts @@ -0,0 +1,21 @@ +import type { Game } from "../types/game.js"; +import type { PublicKey } from "../types/public-key.js"; +import { getHeroCost } from "./get-hero-cost.js"; +import { getLevelUpCost } from "./get-level-up-cost.js"; + +export function isBotOutOfMoves(game: Game, publicKey: PublicKey): boolean { + const money = game.playerMoney[publicKey]; + const levelUpCost = getLevelUpCost(game, publicKey); + const cantLevelUp = money < levelUpCost; + const shop = game.playerShops[publicKey] || []; + const minShopEntryPrice = Math.min(...shop.map(getHeroCost)); + const bench = game.playerBenches[publicKey] || {}; + const benchSize = Object.values(bench).filter(Boolean).length; + const benchHasFreeSlots = benchSize < 6; + const cantShopBuy = money < minShopEntryPrice || !benchHasFreeSlots; + const board = game.playerHeroes[publicKey] || []; + const level = game.playerLevel[publicKey] || 1; + const boardHasFreeSlots = board.length < level; + const cantTransposeToBoard = !benchSize || !boardHasFreeSlots; + return cantLevelUp && cantShopBuy && cantTransposeToBoard; +} diff --git a/interface/utils/portray.ts b/interface/utils/portray.ts index 69a4c50..f02d206 100644 --- a/interface/utils/portray.ts +++ b/interface/utils/portray.ts @@ -41,6 +41,7 @@ export function portray( right: false, })) || [], + level: game.playerLevel[publicKey] || 1, players: game.publicKeys.map((p) => ({ name: game.nicknames[p], health: game.playerHealths[p] || 0, diff --git a/interface/utils/render-piece-highlight.ts b/interface/utils/render-piece-highlight.ts index 440f192..2c8c692 100644 --- a/interface/utils/render-piece-highlight.ts +++ b/interface/utils/render-piece-highlight.ts @@ -63,7 +63,13 @@ export function renderPieceHighlight( highlightMesh.position.y = 0.501; } - highlightMesh.visible = true; + highlightMesh.visible = + display.pieces.length < display.level || + display.pieces.some((piece) => piece.transposed) || + (Object.values(display.bench) + .filter(Boolean) + .some((piece) => piece.transposed) && + boardSlotHasPiece); if ( highlightMesh.material !== threeContext.pieceHighlightInactiveMaterial diff --git a/sandbox/utils/display-factory.ts b/sandbox/utils/display-factory.ts index 2f2b9d2..b02bce3 100644 --- a/sandbox/utils/display-factory.ts +++ b/sandbox/utils/display-factory.ts @@ -42,6 +42,7 @@ export class DisplayFactory implements Subscribable { level, isMe, })), + level: 1, bench: [ { hero: { @@ -211,6 +212,7 @@ export class DisplayFactory implements Subscribable { (handle) => handle.id !== piece.hero.id, ); + this.display.level--; return this; }, setTransposed: () => { @@ -240,6 +242,7 @@ export class DisplayFactory implements Subscribable { gui.add(pieceHandle, "removePiece"); this.pieceHandles.push(pieceHandle); + this.display.level++; return this; }