From 4c24f367eb7a4e0ea53685b4b78075a7b6e27050 Mon Sep 17 00:00:00 2001 From: Simon Lemieux <1105380+simlmx@users.noreply.github.com> Date: Tue, 25 Jun 2024 16:30:07 +0000 Subject: [PATCH] backup --- game-template/game/src/index.ts | 61 ++++--- game-template/ui/src/Board.tsx | 6 +- packages/dev-server/src/App.tsx | 36 ++-- packages/dev-server/src/match.ts | 33 ++-- packages/game/src/gameDef.ts | 266 ++++++++++++++++-------------- packages/game/src/testing.ts | 71 +++++--- packages/ui-testing/src/index.tsx | 16 +- packages/ui/src/index.tsx | 122 +++++--------- 8 files changed, 327 insertions(+), 284 deletions(-) diff --git a/game-template/game/src/index.ts b/game-template/game/src/index.ts index a5e139f..f507644 100644 --- a/game-template/game/src/index.ts +++ b/game-template/game/src/index.ts @@ -1,8 +1,10 @@ import { UserId } from "@lefun/core"; import { + makeGameDef, GameDef, - GameMoves, - PlayerMove, + GameState, + PlayerMoveDef, + BoardMoveDefs, } from "@lefun/game"; type Player = { @@ -17,36 +19,56 @@ export type Board = { type EmptyObject = Record; -type GS = { - B: Board; - PB: EmptyObject; - SB: EmptyObject; -}; +type GS = GameState; -type MoveWithArgPayload = { someArg: number }; +type BoardMoveTypes = { + _initMove: EmptyObject; + someBoardMove: EmptyObject; + someBoardMoveWithArgs: { someArg: number }; +}; const moveWithArg = { - execute({ board, userId, payload }) { - // + execute({ board, userId, payload, delayMove }) { + // execute content }, -} satisfies PlayerMove; +} satisfies PlayerMoveDef; const roll = { executeNow({ board, userId }) { board.players[userId].isRolling = true; }, - execute({ board, userId, random, playerboards }) { + execute({ board, userId, random, playerboards, delayMove }) { board.players[userId].diceValue = random.d6(); board.players[userId].isRolling = false; + delayMove({ name: "someBoardMove" }, 100); + delayMove({ name: "someBoardMoveWithArgs", payload: { someArg: 3 } }, 100); }, -} satisfies PlayerMove; +} satisfies PlayerMoveDef; -const moves = { +const playerMoves = { moveWithArg, roll, -} +}; -type GM = typeof moves; +type PM = typeof playerMoves; + +const boardMoves = { + _initMove: { + execute({ board }) { + // + }, + }, + someBoardMove: { + execute({ board }) { + // + }, + }, + someBoardMoveWithArgs: { + execute({ board, payload }) { + // + }, + }, +} satisfies BoardMoveDefs; const game = { initialBoards: ({ players }) => ({ @@ -57,9 +79,10 @@ const game = { ), }, }), - moves, + playerMoves, + boardMoves, minPlayers: 1, maxPlayers: 10, -} satisfies GameDef; +} satisfies GameDef; -export { GS, GM, game }; +export { GS, PM, game }; diff --git a/game-template/ui/src/Board.tsx b/game-template/ui/src/Board.tsx index fcd455b..19493a1 100644 --- a/game-template/ui/src/Board.tsx +++ b/game-template/ui/src/Board.tsx @@ -10,7 +10,7 @@ import { makeUseMakeMove, } from "@lefun/ui"; -import { GS, GM } from "roll-game"; +import { GS, PM } from "roll-game"; import { Trans } from "@lingui/macro"; @@ -19,7 +19,7 @@ const DICE = ["", "\u2680", "\u2681", "\u2682", "\u2683", "\u2684", "\u2685"]; const useSelector = makeUseSelector(); const useSelectorShallow = makeUseSelectorShallow(); -const useMakeMove = makeUseMakeMove(); +const useMakeMove = makeUseMakeMove(); function Player({ userId }: { userId: UserId }) { const itsMe = useSelector((state) => state.userId === userId); @@ -67,7 +67,7 @@ function Board() { - matchRef={matchRef} /> + ); } -function Main>({ +function Main({ gameDef, matchSettings, matchData, }: { - gameDef: GameDef; + gameDef: GameDef; matchSettings: MatchSettings; matchData?: any; }) { @@ -393,7 +395,7 @@ function Main>({ const [loading, setLoading] = useState(true); - const matchRef = useRef | null>(null); + const matchRef = useRef | null>(null); const resetMatch = useCallback( ({ @@ -407,10 +409,11 @@ function Main>({ }) => { const userIds = getUserIds(numPlayers); - let match: Match | null = null; + // FIXME + let match: Match | null = null; if (tryToLoad) { - match = loadMatch(gameDef); + match = loadMatch(gameDef); } const players = Object.fromEntries( @@ -434,7 +437,8 @@ function Main>({ userIds.map((userId, i) => [userId, { color: i.toString() }]), ); - match = new Match({ + // FIXME + match = new Match({ gameDef, matchSettings, matchPlayersSettings, @@ -505,7 +509,7 @@ function Main>({ })} {matchRef && ( - + >({ type AllMessages = Record>; -async function render({ +async function render({ gameDef, board, matchSettings = {}, @@ -531,7 +535,7 @@ async function render({ messages = { en: {} }, }: { // FIXME - gameDef: GameDef; + gameDef: GameDef; board: () => Promise; matchSettings?: MatchSettings; matchData?: any; diff --git a/packages/dev-server/src/match.ts b/packages/dev-server/src/match.ts index ac367c0..bb8b7cf 100644 --- a/packages/dev-server/src/match.ts +++ b/packages/dev-server/src/match.ts @@ -2,24 +2,34 @@ import { Draft, Patch, produceWithPatches } from "immer"; import { createStore, StoreApi } from "zustand"; import { - // Move, Locale, MatchPlayersSettings, MatchSettings, User, UserId, } from "@lefun/core"; -import { GameDef, GameMove, GameState, Random } from "@lefun/game"; +import { + GameDef, + GameStateBase, + PlayerMoveDefs, + PlayerMoveName, + PlayerMoveWithOptionalPayload, + Random, +} from "@lefun/game"; type EmptyObject = Record; -type State = { +type State = { board: GS["B"]; playerboards: Record; secretboard: GS["SB"] | EmptyObject; }; -class Match extends EventTarget { +class Match< + GS extends GameStateBase, + PM extends PlayerMoveDefs, + BMT, +> extends EventTarget { userIds: UserId[]; random: Random; // FIXME @@ -135,7 +145,10 @@ class Match extends EventTarget { } } - makeMove(userId: UserId, move: GameMove) { + makeMove>( + userId: UserId, + move: PlayerMoveWithOptionalPayload, + ) { const now = new Date().getTime(); // Here the `store` is the store for the player making the move, since @@ -149,7 +162,7 @@ class Match extends EventTarget { } const { name, payload } = move; - const { executeNow, execute } = this.gameDef.moves[name]; + const { executeNow, execute } = this.gameDef.playerMoves[name]; const patchesByUserId: Record = Object.fromEntries( this.userIds.map((userId) => [userId, []]), @@ -267,17 +280,17 @@ function separatePatchesByUser( } /* Save match to localStorage */ -function saveMatch(match: Match) { +function saveMatch(match: Match) { const state = match.store.getState(); const userIds = match.userIds; localStorage.setItem("match", JSON.stringify({ state, userIds })); } /* Load match from localStorage */ -function loadMatch( +function loadMatch( // FIXME - gameDef: GameDef, -): Match | null { + gameDef: GameDef, +): Match | null { const data = localStorage.getItem("match"); if (!data) { return null; diff --git a/packages/game/src/gameDef.ts b/packages/game/src/gameDef.ts index b2cd5e1..3c2d619 100644 --- a/packages/game/src/gameDef.ts +++ b/packages/game/src/gameDef.ts @@ -1,7 +1,5 @@ import { AkaType, - // Move, - AnyMove, Credits, EndMatchOptions, GamePlayerSettings, @@ -18,14 +16,9 @@ import { import { Random } from "./random"; -// Think about how to name those: -// GameStateBase? -// GameStateUnknonw? UnkownGameState? -// -export type GameState = { B: unknown; PB: unknown; SB: unknown }; +export type GameStateBase = { B: unknown; PB: unknown; SB: unknown }; -// This one should be "GameState" I think -export type GameStateDefault = { +export type GameState = { B: B; PB: PB; SB: SB; @@ -33,18 +26,6 @@ export type GameStateDefault = { type EmptyObject = Record; -// We provide a default payload for when we don't need one (for example for moves -// without any options). This has the side effect of allowing for a missing -// payload when one should be required. -export const createMove = ( - name: K, -): [K, (payload?: P) => { name: K; payload: P }] => { - const f = (payload: P = {} as any) => { - return { name, payload }; - }; - return [name, f]; -}; - export const DELAYED_MOVE = "lefun/delayedMove"; export type Move = { @@ -55,11 +36,11 @@ export type Move = { /* * Construct a delayed move "action" */ -export const delayedMove = ( - move: Move, +export const delayedMove = >( + move: BoardMoveWithOptionalPayload, // Timestamp (using something like `new Date().getTime()`) ts: number, -): DelayedMove => { +): DelayedMove => { return { type: DELAYED_MOVE, ts, @@ -67,12 +48,12 @@ export const delayedMove = ( }; }; -export type DelayedMove = { +export type DelayedMove> = { type: typeof DELAYED_MOVE; // Time at which we want the move to be executed. ts: number; // The update itself. - move: Move; + move: BoardMoveWithOptionalPayload; }; export type RewardPayload = { @@ -80,29 +61,33 @@ export type RewardPayload = { stats?: Record>; }; -export type SpecialFuncs = { - delayMove: (move: AnyMove, delay: number) => { ts: number }; +// FIXME here we need to extend the board game moves. +export type SpecialFuncs = { + delayMove: >( + move: BoardMoveWithOptionalPayload, + delay: number, + ) => { ts: number }; itsYourTurn: (arg0: ItsYourTurnPayload) => void; endMatch: (arg0?: EndMatchOptions) => void; reward?: (options: RewardPayload) => void; logStat: (key: string, value: number) => void; }; -type ExecuteNowOptions = { +type ExecuteNowOptions = { userId: UserId; - board: G["B"]; + board: GS["B"]; // Assume that the game developer has defined the playerboard if they're using it. - playerboard: G["PB"]; + playerboard: GS["PB"]; payload: P; - delayMove: SpecialFuncs["delayMove"]; + delayMove: SpecialFuncs["delayMove"]; }; -export type ExecuteNow = ( - options: ExecuteNowOptions, +export type ExecuteNow = ( + options: ExecuteNowOptions, // TODO: We should support returning anything and it would be passed to `execute`. ) => void | false; -export type ExecuteOptions = { +export type ExecuteOptions = { userId: UserId; board: G["B"]; // Even though `playerboards` and `secretboard` are optional, we'll assume that the @@ -114,13 +99,17 @@ export type ExecuteOptions = { ts: number; gameData: any; matchData?: any; -} & SpecialFuncs; +} & SpecialFuncs; -export type Execute = ( - options: ExecuteOptions, +export type Execute = ( + options: ExecuteOptions, ) => void; -export type PlayerMove = { +export type PlayerMoveDef< + G extends GameStateBase, + BMT = EmptyObject, + P = EmptyObject, +> = { canDo?: (options: { userId: UserId; board: G["B"]; @@ -129,23 +118,18 @@ export type PlayerMove = { // We'll pass `null` on the client, where we don't have the server time. ts: number | null; }) => boolean; - executeNow?: ExecuteNow; - execute?: Execute; + executeNow?: ExecuteNow; + execute?: Execute; }; -export type BoardExecute = ( - options: Omit, "userId">, +export type BoardExecute = ( + options: Omit, "userId">, ) => void; -export type BoardMove = { - execute?: BoardExecute; +export type BoardMoveDef = { + execute?: BoardExecute; }; -export type BoardMoves = Record< - string, - BoardMove ->; - export type InitialBoardsOptions = { players: UserId[]; matchSettings: MatchSettings; @@ -161,7 +145,7 @@ export type InitialBoardsOptions = { locale: Locale; }; -export type InitialPlayerboardOptions = { +export type InitialPlayerboardOptions = { userId: UserId; board: G["B"]; secretboard: G["SB"]; @@ -206,21 +190,29 @@ export type AutoMoveInfo = { time?: number; }; -export type AutoMoveRet> = +// FIXME any +export type AutoMoveRet< + GS extends GameStateBase, + PM extends PlayerMoveDefs, +> = | { - move: GameMove; + move: PlayerMove; duration?: number; } - | GameMove; + | PlayerMove; -type AutoMoveType> = (arg0: { +type AutoMoveType< + GS extends GameStateBase, + // FIXME + PM extends PlayerMoveDefs, +> = (arg0: { userId: UserId; board: GS["B"]; playerboard: GS["PB"]; secretboard: GS["SB"]; random: Random; returnAutoMoveInfo: boolean; -}) => AutoMoveRet; +}) => AutoMoveRet; type GetAgent = (arg0: { matchSettings: MatchSettings; @@ -228,10 +220,12 @@ type GetAgent = (arg0: { numPlayers: number; }) => Promise>; -// export type AgentGetMoveRet = { -export type AgentGetMoveRet> = { +export type AgentGetMoveRet< + GS extends GameStateBase, + PM extends PlayerMoveDefs, +> = { // The `move` that should be performed. - move: GameMove; + move: PlayerMove; // Some info used for training. autoMoveInfo?: AutoMoveInfo; // How much "thinking time" should be pretend this move took. @@ -262,41 +256,72 @@ export type GetMatchScoreTextOptions = { board: B; }; -// FIXME GameMovesBase -export type GameMoves = Record>; +export type PlayerMoveDefs = { + [key: string]: PlayerMoveDef; +}; -export type MoveName> = Extract< - keyof GM, - string +export type BoardMoveDefs = Record< + keyof BMT, + BoardMoveDef >; -export type MovePayload< - GS extends GameState, - GM extends GameMoves, - K extends MoveName, -> = GM[K] extends PlayerMove ? P : never; - -type GameMove< - GS extends GameState, - GM extends GameMoves, - K extends MoveName = MoveName, +export type PlayerMoveName< + GS extends GameStateBase, + PM extends PlayerMoveDefs, +> = Extract; + +export type PlayerMovePayload< + GS extends GameStateBase, + PM extends PlayerMoveDefs, + K extends PlayerMoveName, +> = PM[K] extends PlayerMoveDef ? P : never; + +type PlayerMove< + GS extends GameStateBase, + PM extends PlayerMoveDefs, + K extends PlayerMoveName = PlayerMoveName, > = { name: K; - payload: MovePayload; + payload: PlayerMovePayload; }; type Optional = Pick, K> & Omit; -export type GameMoveWithOptionalPayload< - GS extends GameState, - GM extends GameMoves, - K extends MoveName, -> = [MovePayload[keyof MovePayload]] extends [never] - ? Optional, "payload"> - : GameMove; +export type PlayerMoveWithOptionalPayload< + GS extends GameStateBase, + PM extends PlayerMoveDefs, + K extends PlayerMoveName, +> = [PlayerMovePayload[keyof PlayerMovePayload]] extends [ + never, +] + ? Optional, "payload"> + : PlayerMove; + +export type BoardMoveName = Extract; + +type BoardMovePayload = BMT[K]; + +export type BoardMove> = { + name: K; + payload: BMT[K]; +}; + +export type BoardMoveWithOptionalPayload> = [ + BoardMovePayload[keyof BoardMovePayload], +] extends [never] + ? Optional, "payload"> + : BoardMove; + +type BMTFromBoardMoveDefs> = + BM extends BoardMoveDefs ? BMT : never; // This is what the game developer must implement. -export type GameDef> = { +export type GameDef< + G extends GameStateBase, + PM extends PlayerMoveDefs, + BM extends BoardMoveDefs = any, + BMT = BMTFromBoardMoveDefs, +> = { initialBoards: (options: InitialBoardsOptions) => { board: G["B"]; playerboards?: Record; @@ -308,8 +333,8 @@ export type GameDef> = { initialPlayerboard?: (options: InitialPlayerboardOptions) => G["PB"]; // For those the key is the `name` of the move/update - moves: GM; - boardMoves?: BoardMoves; + playerMoves: PM; + boardMoves?: BM; gameSettings?: GameSettings; gamePlayerSettings?: GamePlayerSettings; @@ -330,7 +355,7 @@ export type GameDef> = { // 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; + autoMove?: AutoMoveType; getAgent?: GetAgent; // Game-level bot move duration. @@ -355,15 +380,34 @@ export type GameDef> = { stats?: GameStats; }; +// export function makeGameDef< +// GS extends GameStateBase, +// PM extends PlayerMoveDefs, +// BM extends BoardMoveDefs, +// BMT, +// >(gameDef: GameDef) { +// return gameDef; +// } +export const makeGameDef = < + GS extends GameStateBase, + PM extends PlayerMoveDefs, + BM extends BoardMoveDefs, + BMT = BMTFromBoardMoveDefs, +>( + gameDef: GameDef, +) => gameDef; + /* * Internal representation of the game definition. * * When developing a game, use `GameDef` instead. */ -export type GameDef_> = Omit< - GameDef, - "stats" -> & { +export type GameDef_< + GS extends GameStateBase, + PM extends PlayerMoveDefs, + BM extends BoardMoveDefs, + BMT, +> = Omit, "stats"> & { stats?: { allIds: string[]; // Note that we don't have any flags for stats yet, hence the `EmptyObject`. @@ -374,13 +418,16 @@ export type GameDef_> = Omit< /* * Parse a @lefun/game game definition into our internal game definition. */ -export function parseGameDef>( - gameDef: GameDef, -): GameDef_ { +export function parseGameDef< + GS extends GameStateBase, + PM extends PlayerMoveDefs, + BM extends BoardMoveDefs, + BMT, +>(gameDef: GameDef): GameDef_ { // Normalize the stats. const { stats: stats_ } = gameDef; - const stats: GameDef_["stats"] = { + const stats: GameDef_["stats"] = { allIds: [], byId: {}, }; @@ -394,37 +441,6 @@ export function parseGameDef>( return { ...gameDef, stats }; } -// -// Special Move -// - -// This is a special move that game developers can implement rules for. If they -// do, players will be able to join in the middle of a match. -export const [ADD_PLAYER, addPlayer] = createMove< - "lefun/addPlayerMove", - { - userId: UserId; - } ->("lefun/addPlayerMove"); - -// Note that there is also a kickFromMatch "action" in the 'common' package. -export const [KICK_PLAYER, kickPlayer] = createMove< - "lefun/kickPlayer", - { - userId: UserId; - } ->("lefun/kickPlayer"); - -// This is a special move that will be triggered at the start of the match. -// This way games can implement some logic before any player makes a move, for instance -// triggering a delayed move. -export const [INIT_MOVE, initMove] = createMove("lefun/initMove"); - -// Move triggered by the server when we need to abruptly end a match. -export const [MATCH_WAS_ABORTED, matchWasAborted] = createMove( - "lefun/matchWasAborted", -); - // Game Manifest export type GameManifest = { // TODO: It's a string but currently we are assuming it can be parse to an integer! diff --git a/packages/game/src/testing.ts b/packages/game/src/testing.ts index d79e439..34a67a3 100644 --- a/packages/game/src/testing.ts +++ b/packages/game/src/testing.ts @@ -20,15 +20,15 @@ import { AgentGetMoveRet, AutoMoveInfo, AutoMoveRet, + BoardMoveDefs, + BoardMoveName, + BoardMoveWithOptionalPayload, DelayedMove, delayedMove, GameDef, GameDef_, - GameMoves, - GameMoveWithOptionalPayload, - GameState, + GameStateBase, Move, - MoveName, // GameType, // INIT_MOVE, // initMove, @@ -36,13 +36,21 @@ import { // MATCH_WAS_ABORTED, // matchWasAborted, parseGameDef, + PlayerMoveDefs, + PlayerMoveName, + PlayerMoveWithOptionalPayload, // PlayerMove, RewardPayload, } from "./gameDef"; import { Random } from "./random"; -type MatchTesterOptions> = { - gameDef: GameDef; +type MatchTesterOptions< + G extends GameStateBase, + PM extends PlayerMoveDefs, + BM extends BoardMoveDefs, + BMT, +> = { + gameDef: GameDef; gameData?: any; matchData?: any; numPlayers: number; @@ -86,12 +94,19 @@ type UsersState = { byId: Record }; // payload: M[keyof M]; // }; +type EmptyObject = Record; + /* * Use this to test your game rules. * It emulates what the backend does. */ -export class MatchTester> { - gameDef: GameDef_; +export class MatchTester< + GS extends GameStateBase, + PM extends PlayerMoveDefs, + BM extends BoardMoveDefs = BoardMoveDefs, + BMT = EmptyObject, +> { + gameDef: GameDef_; gameData: any; matchData?: any; meta: Meta; @@ -106,7 +121,7 @@ export class MatchTester> { // Clock used for delayedMoves - in ms. time: number; // List of timers to be executed. - delayedMoves: DelayedMove[]; + delayedMoves: DelayedMove[]; // To help generate the next userIds. nextUserId: number; // Variables to check for infinite loops. @@ -135,7 +150,7 @@ export class MatchTester> { training = false, logBoardToTrainingLog = false, locale = "en", - }: MatchTesterOptions) { + }: MatchTesterOptions) { if (random == null) { random = new Random(); } @@ -371,9 +386,9 @@ export class MatchTester> { this._isPlaying = false; } - async makeMoveAndContinue>( + async makeMoveAndContinue>( userId: UserId, - move: GameMoveWithOptionalPayload, + move: PlayerMoveWithOptionalPayload, { canFail = false }: { canFail?: boolean } = {}, ) { this.makeMove(userId, move, { canFail }); @@ -399,9 +414,12 @@ export class MatchTester> { metaItsYourTurn(this.meta, payload); }; - // fIXME any - const delayMove = (move: any, delay: number) => { - const dm = delayedMove(move, this.time + delay); + // FIXME any + const delayMove = >( + move: BoardMoveWithOptionalPayload, + delay: number, + ) => { + const dm = delayedMove(move, this.time + delay); // In the match tester, we only note the delayed move. We'll execute them only if // we `fastForward`. this.delayedMoves.push(dm); @@ -415,8 +433,9 @@ export class MatchTester> { return { delayMove, itsYourTurn, endMatch, reward, logStat }; } - // FIXME - makeBoardMove(move: any) { + makeBoardMove>( + move: BoardMoveWithOptionalPayload, + ) { const { name, payload } = move; const { gameDef, @@ -469,12 +488,12 @@ export class MatchTester> { } } - makeMove>( + makeMove>( userId: UserId, - move: GameMoveWithOptionalPayload, + move: PlayerMoveWithOptionalPayload, { canFail = false }: { canFail?: boolean } = {}, ) { - const { name, payload } = move; + const { name, payload = {} } = move; const { board, @@ -488,7 +507,7 @@ export class MatchTester> { time, } = this; - if (!gameDef.moves[name]) { + if (!gameDef.playerMoves[name]) { throw new Error(`game does not implement ${name}`); } @@ -497,9 +516,9 @@ export class MatchTester> { throw new Error(`unknown userId ${userId}`); } - const { moves } = gameDef; + const { playerMoves } = gameDef; - const moveDef = moves[name]; + const moveDef = playerMoves[name]; if (!moveDef) { throw new Error(`unknown move ${name}`); @@ -507,7 +526,7 @@ export class MatchTester> { const playerboard = playerboards[userId]; - const { canDo, executeNow, execute } = gameDef.moves[name]; + const { canDo, executeNow, execute } = gameDef.playerMoves[name]; if ( canDo !== undefined && @@ -612,7 +631,7 @@ export class MatchTester> { // let autoMoveRet: ReturnType['getMove']>; // FIXME - let autoMoveRet: AutoMoveRet | AgentGetMoveRet; + let autoMoveRet: AutoMoveRet | AgentGetMoveRet; const t0 = new Date().getTime(); if (gameDef.autoMove !== undefined) { // TODO deprecate the `autoMove` function in favor of the AutoMover class? @@ -713,7 +732,7 @@ export class MatchTester> { const delayedMoves = this.delayedMoves.filter((du) => du.ts <= this.time); // Sort by time, but keep the order in case of equality. delayedMoves - .map((u, i) => [u, i] as [DelayedMove, number]) + .map((u, i) => [u, i] as [DelayedMove, number]) .sort(([u1, i1], [u2, i2]) => Math.sign(u1.ts - u2.ts) || i1 - i2); for (const delayedMove of delayedMoves) { diff --git a/packages/ui-testing/src/index.tsx b/packages/ui-testing/src/index.tsx index 424bb29..e089230 100644 --- a/packages/ui-testing/src/index.tsx +++ b/packages/ui-testing/src/index.tsx @@ -1,24 +1,24 @@ import { i18n } from "@lingui/core"; import { I18nProvider } from "@lingui/react"; import { render as rtlRender, RenderResult } from "@testing-library/react"; -import { ReactNode } from "react"; +import { ElementType, ReactNode } from "react"; import { createStore } from "zustand"; -import { GameState } from "@lefun/game"; +import { GameStateBase } from "@lefun/game"; import { MatchState, setMakeMove, storeContext } from "@lefun/ui"; -export const render = ( - Board: any, - state: MatchState, +export function render( + Board: ElementType, + state: MatchState, locale: string = "en", -): RenderResult => { +): RenderResult { const userId = state.userId; // Sanity check if (userId == null) { throw new Error("userId should not be null"); } - const store = createStore>()(() => ({ + const store = createStore>()(() => ({ ...state, })); @@ -39,4 +39,4 @@ export const render = ( ); }; return rtlRender(, { wrapper }); -}; +} diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index 36ba198..53da062 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -4,29 +4,15 @@ import { useShallow } from "zustand/react/shallow"; import type { MatchState as _MatchState, UserId } from "@lefun/core"; import type { - // GameDef, - // GameMove, - GameMoves, - GameMoveWithOptionalPayload, - GameState, - GameStateDefault, - MoveName, - // MovePayload, - // FIXME make sure we rename these - // GameStateDefault, - // MoveName, - // MovePayload, - // PlayerMove, + GameStateBase, + PlayerMoveDefs, + PlayerMoveName, + PlayerMoveWithOptionalPayload, } from "@lefun/game"; -type AnyGameType = GameStateDefault; -type UnknownGameType = GameStateDefault; - -// type EmptyObject = Record; - // In the selectors, assume that the boards are defined. We will add a check in the // client code to make sure this is true. -export type MatchState = _MatchState< +export type MatchState = _MatchState< GS["B"], GS["PB"] > & { @@ -35,38 +21,31 @@ export type MatchState = _MatchState< playerboard: GS["PB"]; }; -export type Selector = (state: MatchState) => T; - -// TODO Type this properly -export type Store = StoreApi>; - -// FIXME -export const storeContext = createContext | null>(null); +export type Selector = ( + state: MatchState, +) => T; -// type MakeMove = P extends EmptyObject -// ? (moveName: K, payload: P) => void -// : (moveName: K) => void; +export type Store = StoreApi>; -// let _makeMove: (store: Store) => MakeMove; -// type AnyGameState = GameStateDefault +export const storeContext = createContext | null>(null); let _makeMove: < - GS extends GameState, - GM extends GameMoves, - K extends MoveName, + GS extends GameStateBase, + PM extends PlayerMoveDefs, + GMT, + K extends PlayerMoveName, >( - store: Store, -) => (move: GameMoveWithOptionalPayload) => void; + store: Store, +) => (move: PlayerMoveWithOptionalPayload) => void; -// export function setMakeMove>( export function setMakeMove( makeMove: < - GS extends GameState, - GM extends GameMoves, - K extends MoveName, + GS extends GameStateBase, + PM extends PlayerMoveDefs, + K extends PlayerMoveName, >( - move: GameMoveWithOptionalPayload, - store: Store, + move: PlayerMoveWithOptionalPayload, + store: Store, ) => void, ) { _makeMove = (store) => (move) => { @@ -74,16 +53,11 @@ export function setMakeMove( }; } -// type EmptyObject = Record; - -// type IsEmpty = [T[keyof T]] extends [never] ? true : false; - -// ? Optional, "payload"> - -export function useMakeMove>(): < - K extends MoveName, ->( - move: GameMoveWithOptionalPayload, +export function useMakeMove< + GS extends GameStateBase, + PM extends PlayerMoveDefs, +>(): >( + move: PlayerMoveWithOptionalPayload, ) => void { if (!_makeMove) { throw new Error( @@ -108,17 +82,16 @@ export function useMakeMove>(): < // } export function makeUseMakeMove< - GS extends GameState, - GM extends GameMoves, - // K extends MoveName, + GS extends GameStateBase, + PM extends PlayerMoveDefs, >() { - return useMakeMove; + return useMakeMove; } /* * Main way to get data from the match state. */ -export function useSelector( +export function useSelector( selector: Selector, ): T { const store = useContext(storeContext); @@ -130,13 +103,13 @@ export function useSelector( /* Util to "curry" the types of useSelector<...> */ export const makeUseSelector = - () => + () => (selector: Selector) => useSelector(selector); /* Util to "curry" the types of useSelectorShallow<...> */ export const makeUseSelectorShallow = - () => + () => (selector: Selector) => useSelectorShallow(selector); @@ -144,7 +117,7 @@ export const makeUseSelectorShallow = * Same as `useSelector` but will use a shallow equal on the output to decide if a render * is required or not. */ -export function useSelectorShallow( +export function useSelectorShallow( selector: Selector, ): T { return useSelector(useShallow(selector)); @@ -153,7 +126,7 @@ export function useSelectorShallow( /* * Util to check if the user is a player (if not they are a spectator). */ -export const useIsPlayer = () => { +export const useIsPlayer = () => { // Currently, the user is a player iif its playerboard is defined. const hasPlayerboard = useSelector( (state: _MatchState) => { @@ -201,7 +174,7 @@ const toClientTime = * has happened. This can be useful if you want some action from the server to happen * exactly when a countdown gets to 0. */ -export const useToClientTime = () => { +export const useToClientTime = () => { const delta = useSelector( (state: _MatchState) => state.timeDelta || 0, ); @@ -247,7 +220,7 @@ export const playSound = (name: string) => { /* * Util to get a username given its userId */ -export const useUsername = ( +export const useUsername = ( userId?: UserId, ): string | undefined => { const username = useSelector((state: _MatchState) => { @@ -259,21 +232,16 @@ export const useUsername = ( /* * Return a userId: username mapping. */ -export const useUsernames = (): Record< - UserId, - string -> => { +export const useUsernames = (): Record => { // Note the shallow-compared selector. - const usernames = useSelectorShallow( - (state: _MatchState) => { - const users = state.users.byId; - const usernames: { [userId: string]: string } = {}; - for (const [userId, { username }] of Object.entries(users)) { - usernames[userId] = username; - } - return usernames; - }, - ); + const usernames = useSelectorShallow((state: _MatchState) => { + const users = state.users.byId; + const usernames: { [userId: string]: string } = {}; + for (const [userId, { username }] of Object.entries(users)) { + usernames[userId] = username; + } + return usernames; + }); return usernames; };