import { z } from 'zod'; import { Difficulty, GameMap, PlayerType, UnitType } from './game/Game'; export type GameID = string export type ClientID = string export type Intent = SpawnIntent | AttackIntent | BoatAttackIntent | AllianceRequestIntent | AllianceRequestReplyIntent | BreakAllianceIntent | TargetPlayerIntent | EmojiIntent | DonateIntent | TargetTroopRatioIntent | BuildUnitIntent export type AttackIntent = z.infer export type SpawnIntent = z.infer export type BoatAttackIntent = z.infer export type AllianceRequestIntent = z.infer export type AllianceRequestReplyIntent = z.infer export type BreakAllianceIntent = z.infer export type TargetPlayerIntent = z.infer export type EmojiIntent = z.infer export type DonateIntent = z.infer export type TargetTroopRatioIntent = z.infer export type BuildUnitIntent = z.infer export type Turn = z.infer export type GameConfig = z.infer export type ClientMessage = ClientPingMessage | ClientIntentMessage | ClientJoinMessage export type ServerMessage = ServerSyncMessage | ServerStartGameMessage export type ServerSyncMessage = z.infer export type ServerStartGameMessage = z.infer export type ClientPingMessage = z.infer export type ClientIntentMessage = z.infer export type ClientJoinMessage = z.infer const PlayerTypeSchema = z.nativeEnum(PlayerType); // TODO: create Cell schema export interface Lobby { id: string; msUntilStart?: number; numClients?: number; } const GameConfigSchema = z.object({ gameMap: z.nativeEnum(GameMap), difficulty: z.nativeEnum(Difficulty) }) const EmojiSchema = z.string().refine( (val) => { return /\p{Emoji}/u.test(val); }, { message: "Must contain at least one emoji character" } ); // Zod schemas const BaseIntentSchema = z.object({ type: z.enum(['attack', 'spawn', 'boat', 'name', 'targetPlayer', 'emoji', 'troop_ratio', 'build_unit']), clientID: z.string(), }); export const AttackIntentSchema = BaseIntentSchema.extend({ type: z.literal('attack'), attackerID: z.string(), targetID: z.string().nullable(), troops: z.number().nullable(), sourceX: z.number().nullable(), sourceY: z.number().nullable(), targetX: z.number().nullable(), targetY: z.number().nullable() }); export const SpawnIntentSchema = BaseIntentSchema.extend({ type: z.literal('spawn'), playerID: z.string(), name: z.string(), playerType: PlayerTypeSchema, x: z.number(), y: z.number(), }) export const BoatAttackIntentSchema = BaseIntentSchema.extend({ type: z.literal('boat'), attackerID: z.string(), targetID: z.string().nullable(), troops: z.number().nullable(), x: z.number(), y: z.number(), }) export const UpdateNameIntentSchema = BaseIntentSchema.extend({ type: z.literal('updateName'), name: z.string(), }) export const AllianceRequestIntentSchema = BaseIntentSchema.extend({ type: z.literal('allianceRequest'), requestor: z.string(), recipient: z.string(), }) export const AllianceRequestReplyIntentSchema = BaseIntentSchema.extend({ type: z.literal('allianceRequestReply'), requestor: z.string(), // The one who made the original alliance request recipient: z.string(), accept: z.boolean(), }) export const BreakAllianceIntentSchema = BaseIntentSchema.extend({ type: z.literal('breakAlliance'), requestor: z.string(), // The one who made the original alliance request recipient: z.string(), }) export const TargetPlayerIntentSchema = BaseIntentSchema.extend({ type: z.literal('targetPlayer'), requestor: z.string(), target: z.string(), }) export const EmojiIntentSchema = BaseIntentSchema.extend({ type: z.literal('emoji'), sender: z.string(), recipient: z.string(), emoji: EmojiSchema, }) export const DonateIntentSchema = BaseIntentSchema.extend({ type: z.literal('donate'), sender: z.string(), recipient: z.string(), troops: z.number().nullable(), }) export const TargetTroopRatioIntentSchema = BaseIntentSchema.extend({ type: z.literal('troop_ratio'), player: z.string(), ratio: z.number().min(0).max(1), }) export const BuildUnitIntentSchema = BaseIntentSchema.extend({ type: z.literal('build_unit'), player: z.string(), unit: z.nativeEnum(UnitType), x: z.number(), y: z.number(), }) const IntentSchema = z.union([ AttackIntentSchema, SpawnIntentSchema, BoatAttackIntentSchema, AllianceRequestIntentSchema, AllianceRequestReplyIntentSchema, BreakAllianceIntentSchema, TargetPlayerIntentSchema, EmojiIntentSchema, DonateIntentSchema, TargetTroopRatioIntentSchema, BuildUnitIntentSchema, ]); const TurnSchema = z.object({ turnNumber: z.number(), gameID: z.string(), intents: z.array(IntentSchema) }) // Server const ServerBaseMessageSchema = z.object({ type: z.string() }) export const ServerTurnMessageSchema = ServerBaseMessageSchema.extend({ type: z.literal('turn'), turn: TurnSchema, }) export const ServerStartGameMessageSchema = ServerBaseMessageSchema.extend({ type: z.literal('start'), // Turns the client missed if they are late to the game. turns: z.array(TurnSchema), config: GameConfigSchema }) export const ServerMessageSchema = z.union([ServerTurnMessageSchema, ServerStartGameMessageSchema]); // Client const ClientBaseMessageSchema = z.object({ type: z.string(), clientID: z.string(), gameID: z.string(), }) export const ClientPingMessageSchema = ClientBaseMessageSchema.extend({ type: z.literal('ping'), }) export const ClientIntentMessageSchema = ClientBaseMessageSchema.extend({ type: z.literal('intent'), intent: IntentSchema }) export const ClientJoinMessageSchema = ClientBaseMessageSchema.extend({ type: z.literal('join'), clientIP: z.string().nullable(), lastTurn: z.number() // The last turn the client saw. }) export const ClientMessageSchema = z.union([ClientPingMessageSchema, ClientIntentMessageSchema, ClientJoinMessageSchema]);