Skip to content

Commit

Permalink
Refactor the way we deal with bot moves
Browse files Browse the repository at this point in the history
  • Loading branch information
simlmx committed Jul 22, 2024
1 parent 2050c2d commit b4023b8
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 128 deletions.
3 changes: 2 additions & 1 deletion game-template/game/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"build": "rm -rf ./dist && pnpm rollup --config",
"watch": "pnpm rollup --config --watch",
"format": "pnpm eslint . --fix",
"check-format": "pnpm eslint . --quiet"
"check-format": "pnpm eslint . --quiet",
"test": "vitest run src"
},
"devDependencies": {
"@lefun/core": "workspace:*",
Expand Down
15 changes: 14 additions & 1 deletion game-template/game/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test } from "vitest";
import { expect, test } from "vitest";

import { MatchTester as _MatchTester } from "@lefun/game";

Expand All @@ -13,5 +13,18 @@ test("sanity check", () => {
const userId = Object.keys(players)[0];

match.makeMove(userId, "roll");
match.makeMove(userId, "roll", {}, { canFail: true });
match.makeMove(userId, "moveWithArg", { someArg: "123" });
match.makeMove(userId, "moveWithArg", { someArg: "123" }, { canFail: true });

// Time has no passed yet
expect(match.board.lastSomeBoardMoveValue).toBeUndefined();

// Not enough time
match.fastForward(50);
expect(match.board.lastSomeBoardMoveValue).toBeUndefined();

// Enough time
match.fastForward(50);
expect(match.board.lastSomeBoardMoveValue).toEqual(3);
});
21 changes: 18 additions & 3 deletions game-template/game/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { UserId } from "@lefun/core";
import { BoardMove, Game, GameState, INIT_MOVE, PlayerMove } from "@lefun/game";
import {
AutoMove,
BoardMove,
Game,
GameState,
INIT_MOVE,
PlayerMove,
} from "@lefun/game";

type Player = {
isRolling: boolean;
Expand All @@ -9,6 +16,7 @@ type Player = {
export type Board = {
count: number;
players: Record<UserId, Player>;
lastSomeBoardMoveValue?: number;
};

export type RollGameState = GameState<Board>;
Expand Down Expand Up @@ -53,8 +61,8 @@ const someBoardMoveWithArgs: BoardMove<
BMT["someBoardMoveWithArgs"],
BMT
> = {
execute() {
//
execute({ board, payload }) {
board.lastSomeBoardMoveValue = payload.someArg;
},
};

Expand All @@ -73,4 +81,11 @@ export const game = {
maxPlayers: 10,
} satisfies Game<RollGameState, BMT>;

export const autoMove: AutoMove<RollGameState, RollGame> = ({ random }) => {
if (random.d2() === 1) {
return ["moveWithArg", { someArg: "123" }];
}
return "roll";
};

export type RollGame = typeof game;
119 changes: 80 additions & 39 deletions packages/game/src/gameDef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,6 @@ export type Execute<G extends GameStateBase, P, BMT extends BMTBase> = (
options: ExecuteOptions<G, P, BMT>,
) => void;

// `any` doesn't seem to work but `infer ?` does.
export type GetPayloadOfPlayerMove<PM> =
// eslint-disable-next-line @typescript-eslint/no-unused-vars
PM extends PlayerMove<infer G, infer P, infer BMT> ? P : never;

export type PlayerMove<
G extends GameStateBase,
P = NoPayload,
Expand Down Expand Up @@ -189,40 +184,61 @@ export type AutoMoveInfo = {
time?: number;
};

export type AutoMoveRet =
| {
move: PlayerMoveObj<any>;
duration?: number;
}
| PlayerMoveObj<any>;
export type GetPayload<
G extends Game<any>,
K extends keyof G["playerMoves"] & string,
> =
// eslint-disable-next-line @typescript-eslint/no-unused-vars
G["playerMoves"][K] extends PlayerMove<infer G, infer P, infer BMT>
? P
: never;

/*
* `name: string` if the move doesn't have any payload, [name: string, payload: ?]
* otherwise.
*/
type MoveObj<G extends Game<any>> = {
[K in keyof G["playerMoves"] & string]: IfNever<
GetPayload<G, K>,
K,
[K, GetPayload<G, K>]
>;
}[keyof G["playerMoves"] & string];

type AutoMoveType<GS extends GameStateBase> = (arg0: {
/*
* Stateless function that returns a bot move.
*/
export type AutoMove<GS extends GameStateBase, G extends Game<GS>> = (arg0: {
userId: UserId;
board: GS["B"];
playerboard: GS["PB"];
secretboard: GS["SB"];
random: Random;
returnAutoMoveInfo: boolean;
}) => AutoMoveRet;
withInfo: boolean;
}) => BotMove<G>;

type GetAgent<B, PB> = (arg0: {
/*
* Return a stateful way (a `Agent` class) to get bot moves.
*/
export type GetAgent<GS extends GameStateBase, G extends Game<GS>> = (arg0: {
matchSettings: MatchSettings;
matchPlayerSettings: MatchPlayerSettings;
numPlayers: number;
}) => Promise<Agent<B, PB>>;
}) => Promise<Agent<GS, G>>;

export type AgentGetMoveRet<P = unknown> = {
// The `move` that should be performed.
move: PlayerMoveObj<P>;
// Some info used for training.
autoMoveInfo?: AutoMoveInfo;
// How much "thinking time" should be pretend this move took.
// After having calculated our move, we'll wait the difference before actually
// executing the move so that it takes that much time.
duration?: number;
};
export type BotMove<G extends Game<any>> =
| MoveObj<G>
| {
move: MoveObj<G>;
// Some info used for training.
autoMoveInfo?: AutoMoveInfo;
// How much "thinking time" should be pretend this move took.
// After having calculated our move, we'll wait the difference before actually
// executing the move so that it takes that much time.
duration?: number;
};

export abstract class Agent<B, PB = EmptyObject> {
export abstract class Agent<GS extends GameStateBase, G extends Game<GS>> {
abstract getMove({
board,
playerboard,
Expand All @@ -231,21 +247,19 @@ export abstract class Agent<B, PB = EmptyObject> {
withInfo,
verbose,
}: {
board: B;
playerboard: PB;
board: GS["B"];
playerboard: GS["PB"];
random: Random;
userId: UserId;
withInfo: boolean;
verbose?: boolean;
}): Promise<AgentGetMoveRet>;
}): Promise<BotMove<G>>;
}

export type GetMatchScoreTextOptions<B> = {
board: B;
};

type PlayerMoveObj<P = unknown> = { name: string; payload: P };

// This is what the game developer must implement.
export type Game<
GS extends GameStateBase,
Expand Down Expand Up @@ -283,13 +297,6 @@ export type Game<
// Games can customize the match score representation using this hook.
getMatchScoreText?: (options: GetMatchScoreTextOptions<GS["B"]>) => string;

// Return a move for a given state of the game for a player. This is used for bots and
// could be used to play for an unactive user.
// Not that technically we don't need the `secretboard` in here. In practice sometimes
// we put data in the secretboard to optimize calculations.
autoMove?: AutoMoveType<GS>;
getAgent?: GetAgent<GS["B"], GS["PB"]>;

// Game-level bot move duration.
botMoveDuration?: number;

Expand Down Expand Up @@ -379,3 +386,37 @@ export type GameManifest = {
};
name?: string;
};

/* Util to parse the diverse format that can take bot moves, as returned by `autoMove`
* and `Agent.getMove`.
*/
export function parseBotMove<G extends Game<any, any>>(
botMove: BotMove<G>,
): {
name: string;
payload?: unknown;
autoMoveInfo?: AutoMoveInfo;
duration?: number;
} {
let name: string;
let payload: unknown = undefined;
let autoMoveInfo: AutoMoveInfo | undefined = undefined;
let duration: number | undefined = undefined;

if (typeof botMove === "string") {
name = botMove;
} else if (Array.isArray(botMove)) {
[name, payload] = botMove;
} else {
({ autoMoveInfo, duration } = botMove);

const { move } = botMove;
if (typeof move === "string") {
name = move;
} else {
[name, payload] = move;
}
}

return { name, payload, autoMoveInfo, duration };
}
Loading

0 comments on commit b4023b8

Please sign in to comment.