start game via ws

This commit is contained in:
evanpelle
2026-04-29 13:00:35 -06:00
parent 58cb86cb6b
commit 6813ea4e1d
6 changed files with 59 additions and 34 deletions
+5 -14
View File
@@ -1011,21 +1011,12 @@ export class HostLobbyModal extends BaseModal {
// If the modal closes as part of starting the game, do not leave the lobby
this.leaveLobbyOnClose = false;
const config = await getRuntimeClientServerConfig();
const response = await fetch(
`${window.location.origin}/${config.workerPath(this.lobbyId)}/api/start_game/${this.lobbyId}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
},
this.dispatchEvent(
new CustomEvent("start-game", {
bubbles: true,
composed: true,
}),
);
if (!response.ok) {
this.leaveLobbyOnClose = true;
}
return response;
}
private kickPlayer(clientID: string) {
+9
View File
@@ -51,6 +51,7 @@ import { TerritoryPatternsModal } from "./TerritoryPatternsModal";
import { TokenLoginModal } from "./TokenLoginModal";
import {
SendKickPlayerIntentEvent,
SendStartGameEvent,
SendUpdateGameConfigIntentEvent,
} from "./Transport";
import { UserSettingModal } from "./UserSettingModal";
@@ -215,6 +216,7 @@ declare global {
interface DocumentEventMap {
"join-lobby": CustomEvent<JoinLobbyEvent>;
"kick-player": CustomEvent;
"start-game": CustomEvent;
"join-changed": CustomEvent;
"open-matchmaking": CustomEvent<undefined>;
}
@@ -311,6 +313,7 @@ class Client {
document.addEventListener("join-lobby", this.handleJoinLobby.bind(this));
document.addEventListener("leave-lobby", this.handleLeaveLobby.bind(this));
document.addEventListener("kick-player", this.handleKickPlayer.bind(this));
document.addEventListener("start-game", this.handleStartGame.bind(this));
document.addEventListener(
"update-game-config",
this.handleUpdateGameConfig.bind(this),
@@ -932,6 +935,12 @@ class Client {
}
}
private handleStartGame() {
if (this.eventBus) {
this.eventBus.emit(new SendStartGameEvent());
}
}
private handleUpdateGameConfig(event: CustomEvent) {
const { config } = event.detail;
+8
View File
@@ -173,6 +173,8 @@ export class SendUpdateGameConfigIntentEvent implements GameEvent {
constructor(public readonly config: Partial<GameConfig>) {}
}
export class SendStartGameEvent implements GameEvent {}
export class Transport {
private socket: WebSocket | null = null;
@@ -262,6 +264,8 @@ export class Transport {
this.eventBus.on(SendUpdateGameConfigIntentEvent, (e) =>
this.onSendUpdateGameConfigIntent(e),
);
this.eventBus.on(SendStartGameEvent, () => this.onSendStartGame());
}
private startPing() {
@@ -644,6 +648,10 @@ export class Transport {
});
}
private onSendStartGame() {
this.sendIntent({ type: "start_game" });
}
private sendIntent(intent: Intent) {
if (this.isLocal || this.socket?.readyState === WebSocket.OPEN) {
const msg = {
+8 -1
View File
@@ -50,7 +50,8 @@ export type Intent =
| DeleteUnitIntent
| KickPlayerIntent
| TogglePauseIntent
| UpdateGameConfigIntent;
| UpdateGameConfigIntent
| StartGameIntent;
export type AttackIntent = z.infer<typeof AttackIntentSchema>;
export type CancelAttackIntent = z.infer<typeof CancelAttackIntentSchema>;
@@ -84,6 +85,7 @@ export type TogglePauseIntent = z.infer<typeof TogglePauseIntentSchema>;
export type UpdateGameConfigIntent = z.infer<
typeof UpdateGameConfigIntentSchema
>;
export type StartGameIntent = z.infer<typeof StartGameIntentSchema>;
export type Turn = z.infer<typeof TurnSchema>;
export type GameConfig = z.infer<typeof GameConfigSchema>;
@@ -453,6 +455,10 @@ export const UpdateGameConfigIntentSchema = z.object({
config: GameConfigSchema.partial(),
});
export const StartGameIntentSchema = z.object({
type: z.literal("start_game"),
});
const IntentSchema = z.discriminatedUnion("type", [
AttackIntentSchema,
CancelAttackIntentSchema,
@@ -478,6 +484,7 @@ const IntentSchema = z.discriminatedUnion("type", [
KickPlayerIntentSchema,
TogglePauseIntentSchema,
UpdateGameConfigIntentSchema,
StartGameIntentSchema,
]);
// StampedIntent = Intent with server-stamped clientID (used in turns and execution)
+29
View File
@@ -487,6 +487,35 @@ export class GameServer {
this.updateGameConfig(stampedIntent.config);
return;
}
case "start_game": {
if (client.clientID !== this.lobbyCreatorID) {
this.log.warn(`Only lobby creator can start game`, {
clientID: client.clientID,
creatorID: this.lobbyCreatorID,
gameID: this.id,
});
return;
}
if (this.isPublic()) {
this.log.warn(`Cannot start public game via WebSocket`, {
gameID: this.id,
});
return;
}
if (this.hasStarted()) {
this.log.warn(`Cannot start game that has already started`, {
gameID: this.id,
clientID: client.clientID,
});
return;
}
this.log.info(`Lobby creator starting game via WebSocket`, {
creatorID: client.clientID,
gameID: this.id,
});
this.start();
return;
}
case "toggle_pause": {
// Only lobby creator can pause/resume
if (client.clientID !== this.lobbyCreatorID) {
-19
View File
@@ -210,25 +210,6 @@ export async function startWorker() {
res.json(game.gameInfo());
});
// Add other endpoints from your original server
app.post("/api/start_game/:id", async (req, res) => {
log.info(`starting private lobby with id ${req.params.id}`);
const game = gm.game(req.params.id);
if (!game) {
return;
}
if (game.isPublic()) {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const clientIP = req.ip || req.socket.remoteAddress || "unknown";
log.info(
`cannot start public game ${game.id}, game is public, ip: ${ipAnonymize(clientIP)}`,
);
return;
}
game.start();
res.status(200).json({ success: true });
});
app.get("/api/game/:id/exists", async (req, res) => {
const lobbyId = req.params.id;
res.json({