Skip to content

Instantly share code, notes, and snippets.

@CGamesPlay
Created February 25, 2022 23:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CGamesPlay/23597a65e01312aa038d1d7999ece799 to your computer and use it in GitHub Desktop.
Save CGamesPlay/23597a65e01312aa038d1d7999ece799 to your computer and use it in GitHub Desktop.
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));
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