From ac46c786c9c49e8faf6d12b37f058153c9e9e01f Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sat, 12 Oct 2024 18:20:41 -0700 Subject: [PATCH] implement single player --- TODO.txt | 6 +- src/client/Client.ts | 2 +- src/client/ClientGame.ts | 15 +-- src/{core => client}/LocalServer.ts | 32 +++---- src/client/PublicLobby.ts | 2 +- src/client/SinglePlayerModal.ts | 9 +- src/client/Transport.ts | 44 +++++++-- src/core/GameSocket.ts | 138 ---------------------------- 8 files changed, 63 insertions(+), 185 deletions(-) rename src/{core => client}/LocalServer.ts (58%) delete mode 100644 src/core/GameSocket.ts diff --git a/TODO.txt b/TODO.txt index e6e7213d4..a65beec78 100644 --- a/TODO.txt +++ b/TODO.txt @@ -163,8 +163,10 @@ * donate troops button DONE 10/7/2024 * Make fake humans spawn by their country DONE 10/9/2024 * UI: leader board DONE 10/12/2024 -* single player menu -* private game menu +* single player mode DONE 10/12/2024 +* single player select map +* implement private game +* private game can select map * optimize sendBoat function * Test on android * NPC more likely to accept alliance fewer alliance player has diff --git a/src/client/Client.ts b/src/client/Client.ts index 0bc6e7c2c..37b545d7f 100644 --- a/src/client/Client.ts +++ b/src/client/Client.ts @@ -67,7 +67,7 @@ class Client { ]); console.log(`got ip ${clientIP}`) this.game = createClientGame( - false, + event.detail.singlePlayer, (): string => {return this.usernameInput.getCurrentUsername()}, uuidv4(), uuidv4(), diff --git a/src/client/ClientGame.ts b/src/client/ClientGame.ts index fd1ea667a..841fbd801 100644 --- a/src/client/ClientGame.ts +++ b/src/client/ClientGame.ts @@ -13,9 +13,6 @@ import {WinCheckExecution} from "../core/execution/WinCheckExecution"; import {SendAttackIntentEvent, SendSpawnIntentEvent, Transport} from "./Transport"; import {createCanvas} from "./graphics/Utils"; import {DisplayMessageEvent, MessageType} from "./graphics/layers/EventsDisplay"; -import {LocalSocket, LocalSocketFactory, SocketFactory, WebsocketFactory as WebSocketFactory} from "../core/GameSocket"; -import {LocalServer} from "../core/LocalServer"; - export function createClientGame(isLocal: boolean, playerName: () => string, clientID: ClientID, playerID: PlayerID, ip: string | null, gameID: GameID, config: Config, terrainMap: TerrainMap): ClientGame { @@ -25,17 +22,7 @@ export function createClientGame(isLocal: boolean, playerName: () => string, cli const canvas = createCanvas() let gameRenderer = createRenderer(canvas, game, eventBus, clientID) - let wsFactory: SocketFactory = null - - if (isLocal) { - wsFactory = new LocalSocketFactory(new LocalServer(config)) - } else { - const wsHost = process.env.WEBSOCKET_URL || window.location.host; - const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - wsFactory = new WebSocketFactory(`${wsProtocol}//${wsHost}`) - } - - const transport = new Transport(wsFactory, eventBus, gameID, clientID, playerID, playerName) + const transport = new Transport(isLocal, eventBus, gameID, clientID, playerID, config, playerName) return new ClientGame( diff --git a/src/core/LocalServer.ts b/src/client/LocalServer.ts similarity index 58% rename from src/core/LocalServer.ts rename to src/client/LocalServer.ts index d75b14882..e45f55c45 100644 --- a/src/core/LocalServer.ts +++ b/src/client/LocalServer.ts @@ -1,26 +1,26 @@ -import {Config} from "./configuration/Config"; -import {GameMessageEvent, LocalSocket} from "./GameSocket"; -import {ClientMessage, ClientMessageSchema, Intent, ServerTurnMessageSchema, Turn} from "./Schemas"; +import {Config} from "../core/configuration/Config"; +import {ClientMessage, ClientMessageSchema, Intent, ServerMessage, ServerTurnMessageSchema, Turn} from "../core/Schemas"; export class LocalServer { private gameID = "LOCAL" - public localSocket: LocalSocket private turns: Turn[] = [] private intents: Intent[] = [] private endTurnIntervalID - - - constructor(private config: Config) { - this.endTurnIntervalID = setInterval(() => this.endTurn(), this.config.turnIntervalMs()); + constructor(private config: Config, private clientConnect: () => void, private clientMessage: (message: ServerMessage) => void) { } - onConnect() { - + start() { + this.endTurnIntervalID = setInterval(() => this.endTurn(), this.config.turnIntervalMs()); + this.clientConnect() + this.clientMessage({ + type: "start", + turns: [], + }) } onMessage(message: string) { @@ -38,13 +38,9 @@ export class LocalServer { } this.turns.push(pastTurn) this.intents = [] - - const msg = JSON.stringify(ServerTurnMessageSchema.parse( - { - type: "turn", - turn: pastTurn - } - )) - this.localSocket.onmessage(new GameMessageEvent(msg)) + this.clientMessage({ + type: "turn", + turn: pastTurn + }) } } \ No newline at end of file diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts index ab26c39e6..23d0fe76b 100644 --- a/src/client/PublicLobby.ts +++ b/src/client/PublicLobby.ts @@ -108,7 +108,7 @@ export class PublicLobby extends LitElement { if (this.currLobby == null) { this.currLobby = lobby this.dispatchEvent(new CustomEvent('join-lobby', { - detail: {lobby: lobby}, + detail: {lobby: lobby, singlePlayer: false}, bubbles: true, composed: true })); diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index 61576eb28..cc2c7fbb8 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -83,8 +83,13 @@ button:hover { private startGame() { console.log('Starting single player game...'); - this.dispatchEvent(new CustomEvent('single-player', { - detail: {todo: "TODO"}, + this.dispatchEvent(new CustomEvent('join-lobby', { + detail: { + singlePlayer: true, + lobby: { + id: "LOCAL", + } + }, bubbles: true, composed: true })); diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 9af3260b7..faf45845a 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -1,7 +1,8 @@ +import {Config} from "../core/configuration/Config" import {EventBus, GameEvent} from "../core/EventBus" import {AllianceRequest, AllPlayers, Cell, Player, PlayerID, PlayerType} from "../core/game/Game" import {ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema} from "../core/Schemas" -import {SocketFactory} from "../core/GameSocket" +import {LocalServer} from "./LocalServer" export class SendAllianceRequestIntentEvent implements GameEvent { @@ -68,15 +69,17 @@ export class SendDonateIntentEvent implements GameEvent { export class Transport { - public onconnect: () => {} private socket: WebSocket + private localServer: LocalServer + constructor( - private socketFactory: SocketFactory, + private isLocal: boolean, private eventBus: EventBus, private gameID: GameID, private clientID: ClientID, private playerID: PlayerID, + private config: Config, private playerName: () => string, ) { this.eventBus.on(SendAllianceRequestIntentEvent, (e) => this.onSendAllianceRequest(e)) @@ -91,7 +94,22 @@ export class Transport { } connect(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) { - this.socket = this.socketFactory.createSocket() + if (this.isLocal) { + this.connectLocal(onconnect, onmessage, isActive) + } else { + this.connectRemote(onconnect, onmessage, isActive) + } + } + + private connectLocal(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) { + this.localServer = new LocalServer(this.config, onconnect, onmessage) + this.localServer.start() + } + + private connectRemote(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) { + const wsHost = process.env.WEBSOCKET_URL || window.location.host; + const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + this.socket = new WebSocket(`${wsProtocol}//${wsHost}`) this.socket.onopen = () => { console.log('Connected to game server!'); onconnect() @@ -115,7 +133,7 @@ export class Transport { } joinGame(clientIP: string | null, numTurns: number) { - this.socket.send( + this.sendMsg( JSON.stringify( ClientJoinMessageSchema.parse({ type: "join", @@ -136,7 +154,7 @@ export class Transport { clientID: this.clientID, gameID: this.gameID, }) - this.socket.send(JSON.stringify(msg)) + this.sendMsg(JSON.stringify(msg)) } else { console.log('WebSocket is not open. Current state:', this.socket.readyState); console.log('attempting reconnect') @@ -239,17 +257,25 @@ export class Transport { } private sendIntent(intent: Intent) { - if (this.socket.readyState === WebSocket.OPEN) { + if (this.isLocal || this.socket.readyState === WebSocket.OPEN) { const msg = ClientIntentMessageSchema.parse({ type: "intent", clientID: this.clientID, gameID: this.gameID, intent: intent }) - this.socket.send(JSON.stringify(msg)) + this.sendMsg(JSON.stringify(msg)) } else { console.log('WebSocket is not open. Current state:', this.socket.readyState); console.log('attempting reconnect') } } -} + + private sendMsg(msg: string) { + if (this.isLocal) { + this.localServer.onMessage(msg) + } else { + this.socket.send(msg) + } + } +} \ No newline at end of file diff --git a/src/core/GameSocket.ts b/src/core/GameSocket.ts deleted file mode 100644 index 309d6e436..000000000 --- a/src/core/GameSocket.ts +++ /dev/null @@ -1,138 +0,0 @@ -import {LocalServer} from "./LocalServer"; - -export interface SocketFactory { - createSocket(): WebSocket; -} - -export interface Socket { - onopen: ((event: Event) => void) | null; - onmessage: ((event: MessageEvent) => void) | null; - onerror: ((event: Event) => void) | null; - onclose: ((event: CloseEvent) => void) | null; - readyState: number; - - connect(url: string): void; - send(data: string): void; - close(code?: number, reason?: string): void; -} - -export const WebSocketReadyState = { - CONNECTING: 0, - OPEN: 1, - CLOSING: 2, - CLOSED: 3 -}; - -export class WebsocketFactory implements SocketFactory { - - constructor(private url: string) { } - - createSocket(): WebSocket { - return new WebSocket(this.url) - } -} - -export class LocalSocketFactory implements SocketFactory { - constructor(private localServer: LocalServer) { } - - createSocket(): WebSocket { - return new LocalSocket(this.localServer) - } -} - -export class LocalSocket implements WebSocket { - - - constructor(private server: LocalServer) { - server.localSocket = this - } - - binaryType: BinaryType; - bufferedAmount: number; - extensions: string; - onclose: (this: WebSocket, ev: CloseEvent) => any; - onerror: (this: WebSocket, ev: Event) => any; - onmessage: (this: WebSocket, ev: MessageEvent) => any; - onopen: (this: WebSocket, ev: Event) => any; - protocol: string; - readyState: number; - url: string; - close(code?: number, reason?: string): void { - // this.server.onclose(new GameCloseEvent()) - } - send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void { - this.server.onMessage(data as string) - } - CONNECTING: 0; - OPEN: 1; - CLOSING: 2; - CLOSED: 3; - addEventListener(type: unknown, listener: unknown, options?: unknown): void { - throw new Error("Method not implemented."); - } - removeEventListener(type: unknown, listener: unknown, options?: unknown): void { - throw new Error("Method not implemented."); - } - dispatchEvent(event: Event): boolean { - throw new Error("Method not implemented."); - } - -} - -export class GameMessageEvent implements MessageEvent { - - readonly data: any; - readonly origin: string; - readonly lastEventId: string; - readonly source: WindowProxy | null; - readonly ports: ReadonlyArray; - - constructor(data: any) { - this.data = data; - } - returnValue: boolean; - srcElement: EventTarget; - initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void { - throw new Error("Method not implemented."); - } - NONE: 0; - CAPTURING_PHASE: 1; - AT_TARGET: 2; - BUBBLING_PHASE: 3; - - // MessageEvent interface methods - initMessageEvent(type: string, bubbles?: boolean, cancelable?: boolean, data?: any, origin?: string, lastEventId?: string, source?: WindowProxy | null, ports?: MessagePort[]): void { - // This method is deprecated, so we'll leave it as a no-op - console.warn('initMessageEvent is deprecated'); - } - - // Event interface properties and methods - readonly bubbles: boolean = false; - readonly cancelBubble: boolean = false; - readonly cancelable: boolean = false; - readonly composed: boolean = false; - readonly currentTarget: EventTarget | null = null; - readonly defaultPrevented: boolean = false; - readonly eventPhase: number = Event.NONE; - readonly isTrusted: boolean = false; - readonly target: EventTarget | null = null; - readonly timeStamp: number = Date.now(); - readonly type: string = 'message'; - - // Event interface methods - composedPath(): EventTarget[] { - return []; - } - - preventDefault(): void { - // No-op for this example - } - - stopImmediatePropagation(): void { - // No-op for this example - } - - stopPropagation(): void { - // No-op for this example - } -} \ No newline at end of file