Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions src/common/botwrapper.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { State } from 'boardgame.io';
import { Game, State } from 'boardgame.io';
import { Bot } from 'boardgame.io/ai';
import { BotAction } from 'boardgame.io/dist/types/src/ai/bot';
import { PlayerIDType } from './types';

// Determine the next move for the bot and which move function to use.
type BotStrategy<T_SpecificGameState, T_Move> = (state: State<T_SpecificGameState>, botID: string) => [T_Move | undefined, string];


export type SpecificBot<T_SpecificGameState, T_Move> = ReturnType<typeof botWrapper<T_SpecificGameState, T_Move>>

/// wraps a convenient strategy to a full Boardgame.io Bot class
/// @param strategy Must calculate the move to be made or `undefined` if a random move is to be made
/// @result a Boardgame.io Bot class
export default function botWrapper<T_SpecificGameState, T_Move>(botstrategy: BotStrategy<T_SpecificGameState, T_Move>) {
export default function botWrapper<T_SpecificGameState, T_Move>(botstrategy: BotStrategy<T_SpecificGameState, T_Move>)
{
//this actually results in a somewhat correct type, altough it doesn't implements some of the private methods, for some reason ts doesn't complains about that
class _Bot extends Bot {
// waits 400 ms for UX
async wait(): Promise<void> {
private async wait(): Promise<void> {
await new Promise(resolve => setTimeout(resolve, 400));
}

async play(state: State<T_SpecificGameState>, playerID: string): Promise<{ action: BotAction; metadata?: any; }> {
async play(state: State<T_SpecificGameState>, playerID: PlayerIDType): Promise<{ action: BotAction; metadata?: any; }> {
await this.wait();
const [move, moveName] = botstrategy(state, playerID);
if (move === undefined) {
Expand All @@ -37,4 +43,21 @@ export default function botWrapper<T_SpecificGameState, T_Move>(botstrategy: Bot
}
return _Bot;
}
// TODO: accept more than one move
// TODO: accept more than one move

//Heresy in the name of ''''''type safety'''''
export type MergedWrappedBots<T> = T extends (SpecificBot<infer U,infer M1>|SpecificBot<infer V,infer M2>)[] ? SpecificBot<U|V,M1|M2>[] : never;

//Most unholiest possible trick in the name of ''''''type safety'''''
export type ChangeReturnType<T> = T extends (...args: infer Args) => any ? (...args: Args) => BotAction[] : never;

export function makeWrappedBot<T_SpecificGameState, T_Move>(
wrappedFactory:SpecificBot<T_SpecificGameState, T_Move>,
enumerate: NonNullable<Game['ai']>['enumerate'], //<- this is in fact inconsistent with itself for matter of fact this is not equal to Bot['enumerate]
seed?: Game['seed']):
InstanceType<SpecificBot<T_SpecificGameState, T_Move>>{
return new (wrappedFactory)({
enumerate: enumerate,
seed: seed,
})
}
13 changes: 7 additions & 6 deletions src/common/game_for_testing.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { Ctx } from "boardgame.io";
import { RandomAPI } from "boardgame.io/dist/types/src/plugins/random/random";
import { GameStateMixin, GameType, SetupFunction, StartingPositionFunction } from "./types";
import { BruteForcedPlayerIDExtension, GameStateMixin, GameType, PlayerIDType, SetupFunction, StartingPositionFunction } from "./types";

type G = { data: string };

export function createGameWithMoveWithoutStartingPosition(setup: SetupFunction<G>,
move: ({ G, ctx, playerID, random }: { G: G, ctx: Ctx; playerID: string; random: RandomAPI; }, ...args: any[]) => GameStateMixin & G): GameType<G> {
move: ({ G, ctx, playerID, random }: { G: G, ctx: Ctx, playerID: string, random: RandomAPI, }, ...args: any[]) => GameStateMixin & G
): GameType<G> {
// Wraps move in a function so that it is registered as function (solves `invalid move object` error)
const game: GameType<G> = {
name: "stub-game",
setup,
moves: {
move: (...args) => move(...args),
move: (...args) => move(...args), // TODO make the as type here to work, so it can be used with the proper signature
},
possibleMoves: () => [null, "move"],
possibleMoves: () => [],
};
return game;
}
Expand All @@ -24,11 +25,11 @@ export function createGameWithMove(setup: SetupFunction<G>, startingPosition: St
const game: GameType<G> = {
name: "stub-game",
setup,
startingPosition: (...args) => startingPosition(...args),
startingPosition: (...args) => startingPosition(...args), // TODO make the as type here to work, so it can be used with the proper signature
moves: {
move: (...args) => move(...args),
},
possibleMoves: () => [null, "move"],
possibleMoves: () => [],
};
return game;
}
Expand Down
21 changes: 12 additions & 9 deletions src/common/gamewrapper.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Ctx, Game } from 'boardgame.io';
import { Ctx, Game} from 'boardgame.io';
import { INVALID_MOVE, TurnOrder } from 'boardgame.io/core';
import { GameStateMixin, GameType } from './types';
import { GameStateMixin, GameType, PlayerIDType } from './types';
import { BotAction } from 'boardgame.io/dist/types/src/ai/bot';

function chooseRole({ G, ctx, playerID }: any, firstPlayer: string):void { // TODO: type

//HMM, type casting there may be not ideal, let's check it later TODO: check it later
function chooseRole({ G, ctx, playerID }:{G:any,ctx:any,playerID:string}, firstPlayer: PlayerIDType):void { // TODO: solve type
G.firstPlayer = firstPlayer;
}

function chooseNewGameType({ G, ctx, playerID, random, events }: any, difficulty: string) {
function chooseNewGameType({ G, ctx, playerID, random, events }: any, difficulty: string) { // TODO: solve type
if (playerID !== "0") {
return INVALID_MOVE;
};
Expand All @@ -23,7 +26,7 @@ function chooseNewGameType({ G, ctx, playerID, random, events }: any, difficulty
}
};

function setStartingPosition({ G, ctx, playerID, random, events }: any, startingPosition: any) { // TODO: type
function setStartingPosition({ G, ctx, playerID, random, events }: any, startingPosition: any) { // TODO: solve type
if (playerID !== "1") {
return INVALID_MOVE;
};
Expand All @@ -34,7 +37,7 @@ function setStartingPosition({ G, ctx, playerID, random, events }: any, starting
};
};

function getTime({ G, ctx, playerID, events }:any){
function getTime({ G, ctx, playerID, events }:any){// TODO: solve types
if (playerID !== "0") {
return INVALID_MOVE;
};
Expand All @@ -43,7 +46,7 @@ function getTime({ G, ctx, playerID, events }:any){



export function gameWrapper<T_SpecificGameState>(game: GameType<T_SpecificGameState>): Game<T_SpecificGameState & GameStateMixin> { // TODO: solve types
export function gameWrapper<T_SpecificGameState>(game: GameType<T_SpecificGameState>): Game<T_SpecificGameState & GameStateMixin> {
return {
setup: () => ({ ...game.setup(), firstPlayer: null, difficulty: null, winner: null, numberOfTries: 0, numberOfLoss: 0, winningStreak: 0, points: 0}),
turn: {
Expand Down Expand Up @@ -88,7 +91,7 @@ export function gameWrapper<T_SpecificGameState>(game: GameType<T_SpecificGameSt
},
},
},
// conflict with boardgameio type, where id is string, instead of playerIDType
ai: { enumerate: game.possibleMoves as (G:T_SpecificGameState,ctx:Ctx,playerID:string)=>any[] }
// conflict with boardgameio type, where id is string, instead of playerIDType, do manual type cast
ai: { enumerate: game.possibleMoves as (G:T_SpecificGameState,ctx:Ctx,playerID:string)=>BotAction[] }
};
};
22 changes: 18 additions & 4 deletions src/common/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { Ctx, MoveMap, TurnConfig } from "boardgame.io";
import { Ctx, Game, MoveMap, TurnConfig } from "boardgame.io";

export type PlayerIDType = "0" | "1";


//these are maybe usefull
type ChangeKeyType<T, Key extends keyof T, NewType> = Omit<T, Key> & { [K in Key]: NewType };
export type BruteForcedPropertyExtension<T, Key extends keyof T, NewType> = T extends Record<Key, T[Key]>
? ChangeKeyType<T, Key, NewType>
: T;

//or these
export type BruteForcedPlayerIDExtension<T> = T extends {playerID:PlayerIDType}
? Omit<T,'playerID'>&{playerID:string}
:T

export interface GameStateMixin {
firstPlayer: null | 0 | 1;
winner: PlayerIDType | "draw" | null;
Expand All @@ -15,10 +27,12 @@ export interface GameStateMixin {
export type SetupFunction<G> = () => G;
export type StartingPositionFunction<G> = (_: {G: G & GameStateMixin; ctx: Ctx; playerID: PlayerIDType; random: any}) => G;

type PossibleMovesReturnType = ReturnType<NonNullable<Game['ai']>['enumerate']>

/// GameWrapper's mixin.
/// setup() is defined here, as it returns G instead of G & WrapperState
interface GameMixin<G> {
possibleMoves: (G: G, ctx: Ctx, playerID: PlayerIDType) => any[];
possibleMoves: (G: G, ctx: Ctx, playerID: PlayerIDType) => PossibleMovesReturnType;
setup: SetupFunction<G>,
startingPosition?: StartingPositionFunction<G>;
}
Expand Down Expand Up @@ -53,6 +67,6 @@ export interface Game<
export type GameType<G> = WrappableGame<G & GameStateMixin> & GameMixin<G>;

/// Allows typing: change ctx.currentPlayer -> currentPlayer(ctx)
export function currentPlayer(ctx: Ctx): "0" | "1" {
return ctx.currentPlayer as "0" | "1";
export function currentPlayer(ctx: Ctx): PlayerIDType {
return ctx.currentPlayer as PlayerIDType;
}
6 changes: 3 additions & 3 deletions src/games/15oc/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ export const MyGame: GameType<MyGameState> = { // TOOO: solve type
},

possibleMoves: (G, ctx, playerID) => {
let moves = [1];
let moves: any[] = [];

return moves;
},
};

function getWinner(): string {
return "";
function getWinner(): PlayerIDType|'draw'|null {
return null;

}
2 changes: 1 addition & 1 deletion src/games/15od/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const MyGame: GameType<MyGameState> = { // TOOO: solve type
},

possibleMoves: (G, ctx, playerID) => {
let moves = [1];
let moves: any[] = [];

return moves;
},
Expand Down
2 changes: 1 addition & 1 deletion src/games/15oe/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const MyGame: GameType<MyGameState> = { // TOOO: solve type
},

possibleMoves: (G, ctx, playerID) => {
let moves = [1];
let moves:any[] = [];

return moves;
},
Expand Down
3 changes: 2 additions & 1 deletion src/games/relay/game.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Game } from "boardgame.io";
import { INVALID_MOVE, TurnOrder } from "boardgame.io/core";
import { BotAction } from "boardgame.io/dist/types/src/ai/bot";

type Answer = {
answer: number;
Expand Down Expand Up @@ -166,7 +167,7 @@ export const GameRelay: Game<MyGameState> = {
},

ai: {
enumerate: (G, ctx, playerID) => {
enumerate: (G:MyGameState, ctx, playerID):BotAction[] => {
return [];
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/games/relay/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ const problems : RelayProblems ={
}

export function strategy(category: "C" | "D" | "E"){
return (state: State<MyGameState>, botID: string): [any[] | undefined, string] => {
return (state: State<MyGameState>, botID: string): [(Number|String|boolean)[] | undefined, string] => {
if (state.G.numberOfTry === 0) {
let url = problems[category][state.G.currentProblem].url;
if(url === undefined){
Expand Down
23 changes: 12 additions & 11 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { argv, env, exit } from 'process';
import { gameWrapper } from './common/gamewrapper';
import { SocketIOButBotMoves } from './socketio_botmoves';
import { Server } from 'boardgame.io/server';
import botWrapper from './common/botwrapper';
import botWrapper, { ChangeReturnType as MergedWrappedBots, makeWrappedBot } from './common/botwrapper';
import { strategy as RelayStrategy } from './games/relay/strategy';
import { strategyWrapper as TenCoinsStrategy } from './games/ten-coins/strategy';
import { configureTeamsRouter } from './server/router';
Expand Down Expand Up @@ -74,12 +74,12 @@ export const strategyNames = {
}

const games = [
{ ...GameRelay, name: relayNames.C },
{ ...GameRelay, name: relayNames.D },
{ ...GameRelay, name: relayNames.E },
{ ...gameWrapper(TenCoinsGame), name: strategyNames.C },
{ ...gameWrapper(TenCoinsGame), name: strategyNames.D },
{ ...gameWrapper(TenCoinsGame), name: strategyNames.E },
{ ...GameRelay, name: relayNames.C } as typeof GameRelay & {name:string},
{ ...GameRelay, name: relayNames.D } as typeof GameRelay & {name:string},
{ ...GameRelay, name: relayNames.E } as typeof GameRelay & {name:string},
{ ...gameWrapper(TenCoinsGame), name: strategyNames.C } as ReturnType<typeof gameWrapper<typeof TenCoinsGame>> & {name:string},
{ ...gameWrapper(TenCoinsGame), name: strategyNames.D } as ReturnType<typeof gameWrapper<typeof TenCoinsGame>> & {name:string},
{ ...gameWrapper(TenCoinsGame), name: strategyNames.E } as ReturnType<typeof gameWrapper<typeof TenCoinsGame>> & {name:string},
];


Expand Down Expand Up @@ -111,10 +111,11 @@ if (argv[2] === "import") {
const botSetup = Object.fromEntries(
games.map((game, idx) =>
[game.name,
new (bot_factories[idx])({
enumerate: game.ai?.enumerate,
seed: game.seed,
})]
makeWrappedBot(
(bot_factories as MergedWrappedBots<typeof bot_factories>)[idx],
game.ai!.enumerate,
game.seed,
)]
));

const server = Server({
Expand Down
Loading