Created
February 25, 2022 23:21
-
-
Save CGamesPlay/23597a65e01312aa038d1d7999ece799 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { z, UserId, Response, Engine } from "./util"; | |
const Gesture = z.enum(["ROCK", "PAPER", "SCISSORS"]); | |
type Gesture = z.infer<typeof Gesture>; | |
const PlayerInfo = z.object({ | |
id: UserId, | |
score: z.number(), | |
gesture: Gesture.nullable(), | |
}); | |
const PlayerState = z.object({ | |
round: z.number(), | |
player1: PlayerInfo, | |
player2: PlayerInfo.nullable(), | |
}); | |
type PlayerState = z.infer<typeof PlayerState>; | |
const e = new Engine<PlayerState>() | |
.namedTypes({ Gesture, PlayerInfo, PlayerState }) | |
.initialize((userId, _ctx) => { | |
return { | |
round: 0, | |
player1: { id: userId, score: 0, gesture: null }, | |
player2: null, | |
}; | |
}) | |
.action("joinGame", { | |
input: z.object({}), | |
resolve: (state, userId, _ctx, _request) => { | |
if (state.player1.id === userId || state.player2?.id === userId) { | |
return Response.error("Already joined"); | |
} | |
if (state.player2 !== null) { | |
return Response.error("Game full"); | |
} | |
state.player2 = { id: userId, score: 0, gesture: null }; | |
return Response.ok(); | |
}, | |
}) | |
.action("chooseGesture", { | |
input: z.object({ gesture: Gesture }), | |
resolve: (state, userId, _ctx, request) => { | |
if (state.player2 === null) { | |
return Response.error("Game not started"); | |
} | |
const player = [state.player1, state.player2].find( | |
(p) => p.id === userId | |
); | |
if (player === undefined) { | |
return Response.error("Invalid player"); | |
} | |
if (player.gesture !== undefined) { | |
return Response.error("Already picked"); | |
} | |
player.gesture = request.gesture; | |
const otherPlayer = | |
userId === state.player1.id ? state.player2 : state.player1; | |
if (otherPlayer.gesture !== null) { | |
if (gestureWins(player.gesture, otherPlayer.gesture)) { | |
player.score++; | |
} else if (gestureWins(otherPlayer.gesture, player.gesture)) { | |
otherPlayer.score++; | |
} | |
} | |
return Response.ok(); | |
}, | |
}) | |
.action("nextRound", { | |
input: z.object({}), | |
resolve: (state, _userId, _ctx, _request) => { | |
if (state.player2 === null) { | |
return Response.error("Game not started"); | |
} | |
if (state.player1.gesture === null || state.player2.gesture === null) { | |
return Response.error("Round still in progress"); | |
} | |
state.round++; | |
state.player1.gesture = null; | |
state.player2.gesture = null; | |
return Response.ok(); | |
}, | |
}) | |
.getUserState(PlayerState, (state, userId) => { | |
if (state.player2 === null) { | |
return state; | |
} | |
if (state.player1.gesture !== null && state.player2.gesture !== null) { | |
return state; | |
} | |
return { | |
round: state.round, | |
player1: | |
userId === state.player1.id | |
? state.player1 | |
: { ...state.player1, gesture: null }, | |
player2: | |
userId === state.player2.id | |
? state.player2 | |
: { ...state.player2, gesture: null }, | |
}; | |
}); | |
function gestureWins(gesture: Gesture, otherGesture: Gesture) { | |
return ( | |
(gesture === Gesture.enum.ROCK && otherGesture === Gesture.enum.SCISSORS) || | |
(gesture === Gesture.enum.SCISSORS && | |
otherGesture === Gesture.enum.PAPER) || | |
(gesture === Gesture.enum.PAPER && otherGesture === Gesture.enum.ROCK) | |
); | |
} | |
console.log(JSON.stringify(e.toJson(), null, 2)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { z } from "zod"; | |
export * from "zod"; | |
export const UserId = z.object({ userId: z.number() }); | |
export type UserId = z.infer<typeof UserId>; | |
export const Context = z.object({}); | |
export type Context = z.infer<typeof Context>; | |
export class Response { | |
static ok() { | |
return new Response(); | |
} | |
static error(_err: string) { | |
return new Response(); | |
} | |
} | |
export type GetUserStateArgs<S, US extends z.ZodObject<z.ZodRawShape>> = { | |
type: US; | |
resolve: (state: S, userId: UserId) => z.infer<US>; | |
}; | |
export type InitializeArgs<S> = (userId: UserId, ctx: Context) => S; | |
export type ActionArgs<S, I extends z.ZodObject<z.ZodRawShape>> = { | |
input: I; | |
resolve: ( | |
state: S, | |
userId: UserId, | |
ctx: Context, | |
input: z.infer<I> | |
) => Response; | |
}; | |
export class Engine<S> { | |
_namedTypes = new Map<z.ZodSchema<any>, string>(); | |
initFunc!: InitializeArgs<S>; | |
actions: Record<string, ActionArgs<S, z.ZodObject<z.ZodRawShape>>> = {}; | |
_getUserState: GetUserStateArgs<S, z.ZodObject<z.ZodRawShape>> = { | |
type: z.object({}), | |
resolve: (s) => s, | |
}; | |
namedTypes(types: Record<string, z.ZodSchema<any>>): this { | |
for (const k in types) { | |
this._namedTypes.set(types[k], k); | |
} | |
return this; | |
} | |
initialize(args: InitializeArgs<S>): this { | |
this.initFunc = args; | |
return this; | |
} | |
action<I extends z.ZodObject<z.ZodRawShape>>( | |
name: string, | |
args: ActionArgs<S, I> | |
): this { | |
this.actions[name] = args as unknown as ActionArgs< | |
S, | |
z.ZodObject<z.ZodRawShape> | |
>; | |
return this; | |
} | |
getUserState<US extends z.ZodObject<z.ZodRawShape>>( | |
type: US, | |
resolve: (state: S, userId: UserId) => z.infer<US> | |
): this { | |
this._getUserState = { type, resolve }; | |
return this; | |
} | |
/** Returns a valid `server/impl.ts` implementation for the Engine. */ | |
toImpl(): any { | |
class ret {} | |
const _ret: any = ret; | |
_ret.initialize = this.initFunc; | |
for (const actionName in this.actions) { | |
_ret[actionName] = this.actions[actionName].resolve; | |
} | |
_ret.getUserState = this._getUserState.resolve; | |
return ret; | |
} | |
/** Returns a JSON object corresponding to `hathora.yml`. */ | |
toJson() { | |
const ret: any = { types: {}, methods: {} }; | |
const types = new Map<z.ZodSchema<any>, string>(); | |
const getNamedType = (schema: z.ZodSchema<any>): string => { | |
if (types.has(schema)) { | |
return types.get(schema)!; | |
} | |
const name = this._namedTypes.get(schema) ?? `Type${types.size + 1}`; | |
types.set(schema, name); | |
switch ((schema._def as any).typeName) { | |
case "ZodEnum": { | |
const x = schema as z.ZodEnum<[string, ...string[]]>; | |
ret.types[name] = x.options; | |
break; | |
} | |
case "ZodObject": { | |
const x = schema as z.ZodObject<z.ZodRawShape>; | |
const t: any = {}; | |
for (const k in x.shape) { | |
t[k] = getTypeName(x.shape[k]); | |
} | |
ret.types[name] = t; | |
break; | |
} | |
default: | |
ret.types[name] = `UNKNOWN<${(schema._def as any).typeName}>`; | |
} | |
return name; | |
}; | |
const getTypeName = (schema: z.ZodSchema<any>): string => { | |
if (schema === UserId) return "UserId"; | |
switch ((schema as any)._def.typeName) { | |
case "ZodNumber": | |
return "int"; | |
case "ZodNullable": | |
return `${getTypeName((schema as z.ZodNullable<any>).unwrap())}?`; | |
default: | |
return getNamedType(schema); | |
} | |
}; | |
for (const actionName in this.actions) { | |
const args = this.actions[actionName]; | |
const method: any = {}; | |
for (const argName in args.input.shape) { | |
method[argName] = getTypeName(args.input.shape[argName]); | |
} | |
ret.methods[actionName] = method; | |
} | |
ret.userState = getTypeName(this._getUserState.type); | |
return ret; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment