implement single player

This commit is contained in:
evanpelle
2024-10-12 18:20:41 -07:00
parent 399a1482ff
commit ac46c786c9
8 changed files with 63 additions and 185 deletions
+4 -2
View File
@@ -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
+1 -1
View File
@@ -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(),
+1 -14
View File
@@ -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(
@@ -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
})
}
}
+1 -1
View File
@@ -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
}));
+7 -2
View File
@@ -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
}));
+35 -9
View File
@@ -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)
}
}
}
-138
View File
@@ -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<MessagePort>;
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
}
}