diff --git a/.gitignore b/.gitignore index 84f2ae291..61084a4d5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,4 @@ TODO.txt resources/images/.DS_Store resources/.DS_Store .env -.prettierrc -.prettierignore .DS_Store \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..088994811 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +*.png +*.jpg +*.jpeg +*.gif +*.webp +.prettierignore +.gitignore \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/resources/maps/BlackSea.png b/resources/maps/BlackSea.png index 04c05449e..6b8517303 100644 Binary files a/resources/maps/BlackSea.png and b/resources/maps/BlackSea.png differ diff --git a/resources/maps/NorthAmerica.png b/resources/maps/NorthAmerica.png index 3011a3a44..a5f2db738 100644 Binary files a/resources/maps/NorthAmerica.png and b/resources/maps/NorthAmerica.png differ diff --git a/resources/maps/Oceania.png b/resources/maps/Oceania.png index 209d29745..438089e84 100644 Binary files a/resources/maps/Oceania.png and b/resources/maps/Oceania.png differ diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts index da4dfe658..bba66273e 100644 --- a/src/client/PublicLobby.ts +++ b/src/client/PublicLobby.ts @@ -3,6 +3,7 @@ import { customElement, state } from "lit/decorators.js"; import { Lobby } from "../core/Schemas"; import { Difficulty, GameMapType, GameType } from "../core/game/Game"; import { consolex } from "../core/Consolex"; +import { getMapsImage } from "./utilities/Maps"; @customElement("public-lobby") export class PublicLobby extends LitElement { @@ -80,23 +81,33 @@ export class PublicLobby extends LitElement { : "bg-gradient-to-r from-blue-600 to-blue-500"} text-white font-medium rounded-xl transition-opacity duration-200 hover:opacity-90" >
Next Game
-
-
-
- ${lobby.gameConfig.gameMap} +
+ ${lobby.gameConfig.gameMap} +
+
+
+ ${lobby.gameConfig.gameMap} +
-
-
-
- ${lobby.numClients} - ${lobby.numClients === 1 ? "Player" : "Players"} waiting +
+
+ ${lobby.numClients} + ${lobby.numClients === 1 ? "Player" : "Players"} waiting +
-
-
-
- ${timeDisplay} +
+
+ ${timeDisplay} +
diff --git a/src/client/Transport.ts b/src/client/Transport.ts index c13cbbeba..c4fd5f700 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -26,10 +26,6 @@ import { } from "../core/Schemas"; import { LobbyConfig } from "./ClientGameRunner"; import { LocalServer } from "./LocalServer"; -import { UsernameInput } from "./UsernameInput"; -import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal"; -import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal"; -import { SinglePlayerModal } from "./SinglePlayerModal"; import { PlayerView } from "../core/game/GameView"; export class PauseGameEvent implements GameEvent { @@ -175,6 +171,7 @@ export class Transport { ClientPingMessageSchema.parse({ type: "ping", clientID: this.lobbyConfig.clientID, + persistentID: this.lobbyConfig.persistentID, gameID: this.lobbyConfig.gameID, }), ), @@ -311,7 +308,7 @@ export class Transport { this.sendIntent({ type: "allianceRequest", clientID: this.lobbyConfig.clientID, - requestor: event.requestor.id(), + playerID: event.requestor.id(), recipient: event.recipient.id(), }); } @@ -321,7 +318,7 @@ export class Transport { type: "allianceRequestReply", clientID: this.lobbyConfig.clientID, requestor: event.requestor.id(), - recipient: event.recipient.id(), + playerID: event.recipient.id(), accept: event.accepted, }); } @@ -330,7 +327,7 @@ export class Transport { this.sendIntent({ type: "breakAlliance", clientID: this.lobbyConfig.clientID, - requestor: event.requestor.id(), + playerID: event.requestor.id(), recipient: event.recipient.id(), }); } @@ -352,7 +349,7 @@ export class Transport { this.sendIntent({ type: "attack", clientID: this.lobbyConfig.clientID, - attackerID: this.lobbyConfig.playerID, + playerID: this.lobbyConfig.playerID, targetID: event.targetID, troops: event.troops, }); @@ -362,7 +359,7 @@ export class Transport { this.sendIntent({ type: "boat", clientID: this.lobbyConfig.clientID, - attackerID: this.lobbyConfig.playerID, + playerID: this.lobbyConfig.playerID, targetID: event.targetID, troops: event.troops, x: event.cell.x, @@ -374,7 +371,7 @@ export class Transport { this.sendIntent({ type: "targetPlayer", clientID: this.lobbyConfig.clientID, - requestor: this.lobbyConfig.playerID, + playerID: this.lobbyConfig.playerID, target: event.targetID, }); } @@ -383,7 +380,7 @@ export class Transport { this.sendIntent({ type: "emoji", clientID: this.lobbyConfig.clientID, - sender: this.lobbyConfig.playerID, + playerID: this.lobbyConfig.playerID, recipient: event.recipient == AllPlayers ? AllPlayers : event.recipient.id(), emoji: event.emoji, @@ -394,7 +391,7 @@ export class Transport { this.sendIntent({ type: "donate", clientID: this.lobbyConfig.clientID, - sender: event.sender.id(), + playerID: event.sender.id(), recipient: event.recipient.id(), troops: event.troops, }); @@ -404,7 +401,7 @@ export class Transport { this.sendIntent({ type: "troop_ratio", clientID: this.lobbyConfig.clientID, - player: this.lobbyConfig.playerID, + playerID: this.lobbyConfig.playerID, ratio: event.ratio, }); } @@ -413,7 +410,7 @@ export class Transport { this.sendIntent({ type: "build_unit", clientID: this.lobbyConfig.clientID, - player: this.lobbyConfig.playerID, + playerID: this.lobbyConfig.playerID, unit: event.unit, x: event.cell.x, y: event.cell.y, @@ -437,6 +434,7 @@ export class Transport { const msg = ClientSendWinnerSchema.parse({ type: "winner", clientID: this.lobbyConfig.clientID, + persistentID: this.lobbyConfig.persistentID, gameID: this.lobbyConfig.gameID, winner: event.winner, }); @@ -455,6 +453,7 @@ export class Transport { const msg = ClientIntentMessageSchema.parse({ type: "intent", clientID: this.lobbyConfig.clientID, + persistentID: this.lobbyConfig.persistentID, gameID: this.lobbyConfig.gameID, intent: intent, }); diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts index dd1468265..3ddd1eed8 100644 --- a/src/client/graphics/layers/ControlPanel.ts +++ b/src/client/graphics/layers/ControlPanel.ts @@ -52,6 +52,10 @@ export class ControlPanel extends LitElement implements Layer { @state() private _goldPerSecond: number; + private _lastPopulationIncreaseRate: number; + + private _popRateIsIncreasing: boolean = true; + init() { this.attackRatio = 0.2; this.uiState.attackRatio = this.attackRatio; @@ -63,12 +67,19 @@ export class ControlPanel extends LitElement implements Layer { this.setVisibile(true); } - const player = this.game.playerByClientID(this.clientID); + const player = this.game.myPlayer(); if (player == null || !player.isAlive()) { this.setVisibile(false); return; } + const popIncreaseRate = player.population() - this._population; + if (this.game.ticks() % 5 == 0) { + this._popRateIsIncreasing = + popIncreaseRate >= this._lastPopulationIncreaseRate; + this._lastPopulationIncreaseRate = popIncreaseRate; + } + this._population = player.population(); this._maxPopulation = this.game.config().maxPopulation(player); this._gold = player.gold(); @@ -78,6 +89,7 @@ export class ControlPanel extends LitElement implements Layer { this._goldPerSecond = this.game.config().goldAdditionRate(player) * 10; this.currentTroopRatio = player.troops() / player.population(); + this.requestUpdate(); } onAttackRatioChange(newRatio: number) { @@ -163,7 +175,12 @@ export class ControlPanel extends LitElement implements Layer { ${renderTroops(this._population)} / ${renderTroops(this._maxPopulation)} - (+${renderTroops(this.popRate)})(+${renderTroops(this.popRate)})
diff --git a/src/client/index.html b/src/client/index.html index 375c8a83b..8f608969a 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -167,7 +167,7 @@
-
+
diff --git a/src/client/utilities/Maps.ts b/src/client/utilities/Maps.ts index 6325b3073..a7306aaa0 100644 --- a/src/client/utilities/Maps.ts +++ b/src/client/utilities/Maps.ts @@ -5,6 +5,7 @@ import mena from "../../../resources/maps/Mena.png"; import northAmerica from "../../../resources/maps/NorthAmerica.png"; import blackSea from "../../../resources/maps/BlackSea.png"; import africa from "../../../resources/maps/Africa.png"; + main import { GameMapType } from "../../core/game/Game"; export function getMapsImage(map: GameMapType): string { @@ -23,6 +24,8 @@ export function getMapsImage(map: GameMapType): string { return blackSea; case GameMapType.Africa return Africa; + + main default: return ""; } diff --git a/src/core/PseudoRandom.ts b/src/core/PseudoRandom.ts index e24815b07..985de26fd 100644 --- a/src/core/PseudoRandom.ts +++ b/src/core/PseudoRandom.ts @@ -47,4 +47,13 @@ export class PseudoRandom { chance(odds: number): boolean { return this.nextInt(0, odds) == 0; } + + shuffleArray(array: any[]) { + for (let i = array.length - 1; i >= 0; i--) { + const j = Math.floor(this.nextInt(0, i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + + return array; + } } diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 6ebcc2d7d..eb81c6698 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -133,11 +133,12 @@ const BaseIntentSchema = z.object({ "build_unit", ]), clientID: ID, + playerID: ID, }); export const AttackIntentSchema = BaseIntentSchema.extend({ type: z.literal("attack"), - attackerID: ID, + playerID: ID, targetID: ID.nullable(), troops: z.number().nullable(), }); @@ -154,7 +155,7 @@ export const SpawnIntentSchema = BaseIntentSchema.extend({ export const BoatAttackIntentSchema = BaseIntentSchema.extend({ type: z.literal("boat"), - attackerID: ID, + playerID: ID, targetID: ID.nullable(), troops: z.number().nullable(), x: z.number(), @@ -163,52 +164,52 @@ export const BoatAttackIntentSchema = BaseIntentSchema.extend({ export const AllianceRequestIntentSchema = BaseIntentSchema.extend({ type: z.literal("allianceRequest"), - requestor: ID, + playerID: ID, recipient: ID, }); export const AllianceRequestReplyIntentSchema = BaseIntentSchema.extend({ type: z.literal("allianceRequestReply"), requestor: ID, // The one who made the original alliance request - recipient: ID, + playerID: ID, accept: z.boolean(), }); export const BreakAllianceIntentSchema = BaseIntentSchema.extend({ type: z.literal("breakAlliance"), - requestor: ID, // The one who made the original alliance request + playerID: ID, recipient: ID, }); export const TargetPlayerIntentSchema = BaseIntentSchema.extend({ type: z.literal("targetPlayer"), - requestor: ID, + playerID: ID, target: ID, }); export const EmojiIntentSchema = BaseIntentSchema.extend({ type: z.literal("emoji"), - sender: ID, + playerID: ID, recipient: z.union([ID, z.literal(AllPlayers)]), emoji: EmojiSchema, }); export const DonateIntentSchema = BaseIntentSchema.extend({ type: z.literal("donate"), - sender: ID, + playerID: ID, recipient: ID, troops: z.number().nullable(), }); export const TargetTroopRatioIntentSchema = BaseIntentSchema.extend({ type: z.literal("troop_ratio"), - player: ID, + playerID: ID, ratio: z.number().min(0).max(1), }); export const BuildUnitIntentSchema = BaseIntentSchema.extend({ type: z.literal("build_unit"), - player: ID, + playerID: ID, unit: z.nativeEnum(UnitType), x: z.number(), y: z.number(), @@ -267,6 +268,7 @@ export const ServerMessageSchema = z.union([ const ClientBaseMessageSchema = z.object({ type: z.enum(["winner", "join", "intent", "ping", "log"]), clientID: ID, + persistentID: SafeString.nullable(), // WARNING: persistent id is private. gameID: ID, }); @@ -294,7 +296,6 @@ export const ClientIntentMessageSchema = ClientBaseMessageSchema.extend({ // WARNING: never send this message to clients. export const ClientJoinMessageSchema = ClientBaseMessageSchema.extend({ type: z.literal("join"), - persistentID: SafeString, // WARNING: persistent id is private. lastTurn: z.number(), // The last turn the client saw. username: SafeString, }); diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index 6bff9b4b0..0bd6a365f 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -62,6 +62,17 @@ export class AttackExecution implements Execution { } this.mg = mg; + if (!mg.hasPlayer(this._ownerID)) { + console.warn(`player ${this._ownerID} not found`); + this.active = false; + return; + } + if (this._targetID != null && !mg.hasPlayer(this._targetID)) { + console.warn(`target ${this._targetID} not found`); + this.active = false; + return; + } + this._owner = mg.player(this._ownerID); this.target = this._targetID == this.mg.terraNullius().id() @@ -90,8 +101,8 @@ export class AttackExecution implements Execution { .config() .attackAmount(this._owner, this.target); } - this.startTroops = Math.min(this._owner.troops(), this.startTroops); if (this.removeTroops) { + this.startTroops = Math.min(this._owner.troops(), this.startTroops); this._owner.removeTroops(this.startTroops); } this.attack = this._owner.createAttack( diff --git a/src/core/execution/CityExecution.ts b/src/core/execution/CityExecution.ts index 47270708e..d6cbaf2c6 100644 --- a/src/core/execution/CityExecution.ts +++ b/src/core/execution/CityExecution.ts @@ -22,6 +22,11 @@ export class CityExecution implements Execution { init(mg: Game, ticks: number): void { this.mg = mg; + if (!mg.hasPlayer(this.ownerId)) { + console.warn(`CityExecution: player ${this.ownerId} not found`); + this.active = false; + return; + } this.player = mg.player(this.ownerId); } diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts index be86ad275..f6a545528 100644 --- a/src/core/execution/ConstructionExecution.ts +++ b/src/core/execution/ConstructionExecution.ts @@ -36,6 +36,11 @@ export class ConstructionExecution implements Execution { init(mg: Game, ticks: number): void { this.mg = mg; + if (!mg.hasPlayer(this.ownerId)) { + console.warn(`ConstructionExecution: owner ${this.ownerId} not found`); + this.active = false; + return; + } this.player = mg.player(this.ownerId); } diff --git a/src/core/execution/DefensePostExecution.ts b/src/core/execution/DefensePostExecution.ts index d298ebc1e..74001526b 100644 --- a/src/core/execution/DefensePostExecution.ts +++ b/src/core/execution/DefensePostExecution.ts @@ -23,6 +23,11 @@ export class DefensePostExecution implements Execution { init(mg: Game, ticks: number): void { this.mg = mg; + if (!mg.hasPlayer(this.ownerId)) { + console.warn(`DefensePostExectuion: owner ${this.ownerId} not found`); + this.active = false; + return; + } this.player = mg.player(this.ownerId); } diff --git a/src/core/execution/DonateExecution.ts b/src/core/execution/DonateExecution.ts index 0cbefa5da..70012b0e3 100644 --- a/src/core/execution/DonateExecution.ts +++ b/src/core/execution/DonateExecution.ts @@ -14,6 +14,17 @@ export class DonateExecution implements Execution { ) {} init(mg: Game, ticks: number): void { + if (!mg.hasPlayer(this.senderID)) { + console.warn(`DonateExecution: sender ${this.senderID} not found`); + this.active = false; + return; + } + if (!mg.hasPlayer(this.recipientID)) { + console.warn(`DonateExecution recipient ${this.recipientID} not found`); + this.active = false; + return; + } + this.sender = mg.player(this.senderID); this.recipient = mg.player(this.recipientID); if (this.troops == null) { diff --git a/src/core/execution/EmojiExecution.ts b/src/core/execution/EmojiExecution.ts index 4a7147469..1793ce7d2 100644 --- a/src/core/execution/EmojiExecution.ts +++ b/src/core/execution/EmojiExecution.ts @@ -22,6 +22,17 @@ export class EmojiExecution implements Execution { ) {} init(mg: Game, ticks: number): void { + if (!mg.hasPlayer(this.senderID)) { + console.warn(`EmojiExecution: sender ${this.senderID} not found`); + this.active = false; + return; + } + if (this.recipientID != AllPlayers && !mg.hasPlayer(this.recipientID)) { + console.warn(`EmojiExecution: recipient ${this.recipientID} not found`); + this.active = false; + return; + } + this.requestor = mg.player(this.senderID); this.recipient = this.recipientID == AllPlayers ? AllPlayers : mg.player(this.recipientID); diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index b7e3f2c92..e876c9cac 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -33,6 +33,7 @@ import { DonateExecution } from "./DonateExecution"; import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution"; import { ConstructionExecution } from "./ConstructionExecution"; import { fixProfaneUsername, isProfaneUsername } from "../validations/username"; +import { NoOpExecution } from "./NoOpExecution"; export class Executor { // private random = new PseudoRandom(999) @@ -52,11 +53,26 @@ export class Executor { } createExec(intent: Intent): Execution { + if (intent.type != "spawn") { + if (!this.mg.hasPlayer(intent.playerID)) { + console.warn( + `player ${intent.playerID} not found on intent ${intent.type}`, + ); + return new NoOpExecution(); + } + const player = this.mg.player(intent.playerID); + if (player.clientID() != intent.clientID) { + console.warn( + `intent ${intent.type} has incorrect clientID ${intent.clientID} for player ${player.name()} with clientID ${player.clientID()}`, + ); + return new NoOpExecution(); + } + } switch (intent.type) { case "attack": { return new AttackExecution( intent.troops, - intent.attackerID, + intent.playerID, intent.targetID, null, ); @@ -77,40 +93,40 @@ export class Executor { ); case "boat": return new TransportShipExecution( - intent.attackerID, + intent.playerID, intent.targetID, this.mg.ref(intent.x, intent.y), intent.troops, ); case "allianceRequest": - return new AllianceRequestExecution(intent.requestor, intent.recipient); + return new AllianceRequestExecution(intent.playerID, intent.recipient); case "allianceRequestReply": return new AllianceRequestReplyExecution( intent.requestor, - intent.recipient, + intent.playerID, intent.accept, ); case "breakAlliance": - return new BreakAllianceExecution(intent.requestor, intent.recipient); + return new BreakAllianceExecution(intent.playerID, intent.recipient); case "targetPlayer": - return new TargetPlayerExecution(intent.requestor, intent.target); + return new TargetPlayerExecution(intent.playerID, intent.target); case "emoji": return new EmojiExecution( - intent.sender, + intent.playerID, intent.recipient, intent.emoji, ); case "donate": return new DonateExecution( - intent.sender, + intent.playerID, intent.recipient, intent.troops, ); case "troop_ratio": - return new SetTargetTroopRatioExecution(intent.player, intent.ratio); + return new SetTargetTroopRatioExecution(intent.playerID, intent.ratio); case "build_unit": return new ConstructionExecution( - intent.player, + intent.playerID, this.mg.ref(intent.x, intent.y), intent.unit, ); diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 682c3e44f..0b4a02abc 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -546,9 +546,6 @@ export class FakeHumanExecution implements Execution { } sendAttack(toAttack: Player | TerraNullius) { - console.log( - `${this.player.name()} sending troops ${renderTroops(this.player.troops() / 5)}`, - ); this.mg.addExecution( new AttackExecution( this.player.troops() / 5, diff --git a/src/core/execution/MIRVExecution.ts b/src/core/execution/MIRVExecution.ts index 430df2e59..87638fe7b 100644 --- a/src/core/execution/MIRVExecution.ts +++ b/src/core/execution/MIRVExecution.ts @@ -43,6 +43,12 @@ export class MirvExecution implements Execution { ) {} init(mg: Game, ticks: number): void { + if (!mg.hasPlayer(this.senderID)) { + console.warn(`MIRVExecution: player ${this.senderID} not found`); + this.active = false; + return; + } + this.random = new PseudoRandom(mg.ticks() + simpleHash(this.senderID)); this.mg = mg; this.pathFinder = PathFinder.Mini(mg, 10_000, true); diff --git a/src/core/execution/MissileSiloExecution.ts b/src/core/execution/MissileSiloExecution.ts index dd3265262..3dc1a8714 100644 --- a/src/core/execution/MissileSiloExecution.ts +++ b/src/core/execution/MissileSiloExecution.ts @@ -22,6 +22,12 @@ export class MissileSiloExecution implements Execution { ) {} init(mg: Game, ticks: number): void { + if (!mg.hasPlayer(this._owner)) { + console.warn(`MissileSiloExecution: owner ${this._owner} not found`); + this.active = false; + return; + } + this.mg = mg; this.player = mg.player(this._owner); } diff --git a/src/core/execution/NoOpExecution.ts b/src/core/execution/NoOpExecution.ts new file mode 100644 index 000000000..eefce20ed --- /dev/null +++ b/src/core/execution/NoOpExecution.ts @@ -0,0 +1,15 @@ +import { Execution, Game, Player } from "../game/Game"; + +export class NoOpExecution implements Execution { + isActive(): boolean { + return false; + } + activeDuringSpawnPhase(): boolean { + return false; + } + init(mg: Game, ticks: number): void {} + tick(ticks: number): void {} + owner(): Player { + return null; + } +} diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 38f80a933..5f2906120 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -33,6 +33,12 @@ export class NukeExecution implements Execution { ) {} init(mg: Game, ticks: number): void { + if (!mg.hasPlayer(this.senderID)) { + console.warn(`NukeExecution: sender ${this.senderID} not found`); + this.active = false; + return; + } + this.mg = mg; this.player = mg.player(this.senderID); this.random = new PseudoRandom(ticks); @@ -120,18 +126,13 @@ export class NukeExecution implements Execution { return (d <= magnitude.inner || rand.chance(2)) && d <= magnitude.outer; }); - const ratio = Object.fromEntries( - this.mg - .players() - .map((p) => [p.id(), (p.troops() + p.workers()) / p.numTilesOwned()]), - ); const attacked = new Map(); for (const tile of toDestroy) { const owner = this.mg.owner(tile); if (owner.isPlayer()) { const mp = this.mg.player(owner.id()); mp.relinquish(tile); - mp.removeTroops(2 * ratio[mp.id()]); + mp.removeTroops((5 * mp.population()) / mp.numTilesOwned()); if (!attacked.has(mp)) { attacked.set(mp, 0); } diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index 59a69386c..454bc03e4 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -28,6 +28,11 @@ export class PlayerExecution implements Execution { } init(mg: Game, ticks: number) { + if (!mg.hasPlayer(this.playerID)) { + console.warn(`PlayerExecution: player ${this.playerID} not found`); + this.active = false; + return; + } this.mg = mg; this.config = mg.config(); this.player = mg.player(this.playerID); diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index 0d0b99815..18053299a 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -31,6 +31,11 @@ export class PortExecution implements Execution { ) {} init(mg: Game, ticks: number): void { + if (!mg.hasPlayer(this._owner)) { + console.warn(`PortExecution: player ${this._owner} not found`); + this.active = false; + return; + } this.mg = mg; this.random = new PseudoRandom(mg.ticks()); } diff --git a/src/core/execution/SetTargetTroopRatioExecution.ts b/src/core/execution/SetTargetTroopRatioExecution.ts index 191e88f79..495c8ebe6 100644 --- a/src/core/execution/SetTargetTroopRatioExecution.ts +++ b/src/core/execution/SetTargetTroopRatioExecution.ts @@ -12,6 +12,11 @@ export class SetTargetTroopRatioExecution implements Execution { ) {} init(mg: Game, ticks: number): void { + if (!mg.hasPlayer(this.playerID)) { + console.warn( + `SetTargetTRoopRatioExecution: player ${this.playerID} not found`, + ); + } this.player = mg.player(this.playerID); } diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts index 5ee3b0ebc..0c8cc5687 100644 --- a/src/core/execution/SpawnExecution.ts +++ b/src/core/execution/SpawnExecution.ts @@ -28,6 +28,7 @@ export class SpawnExecution implements Execution { this.active = false; if (!this.mg.inSpawnPhase()) { + this.active = false; return; } diff --git a/src/core/execution/TargetPlayerExecution.ts b/src/core/execution/TargetPlayerExecution.ts index f93989aee..ef738b774 100644 --- a/src/core/execution/TargetPlayerExecution.ts +++ b/src/core/execution/TargetPlayerExecution.ts @@ -12,6 +12,19 @@ export class TargetPlayerExecution implements Execution { ) {} init(mg: Game, ticks: number): void { + if (!mg.hasPlayer(this.requestorID)) { + console.warn( + `TargetPlayerExecution: requestor ${this.requestorID} not found`, + ); + this.active = false; + return; + } + if (!mg.hasPlayer(this.targetID)) { + console.warn(`TargetPlayerExecution: target ${this.targetID} not found`); + this.active = false; + return; + } + this.requestor = mg.player(this.requestorID); this.target = mg.player(this.targetID); } diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index fb4f7f4c0..7f9f6e682 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -50,6 +50,19 @@ export class TransportShipExecution implements Execution { } init(mg: Game, ticks: number) { + if (!mg.hasPlayer(this.attackerID)) { + console.warn( + `TransportShipExecution: attacker ${this.attackerID} not found`, + ); + this.active = false; + return; + } + if (this.targetID != null && !mg.hasPlayer(this.targetID)) { + console.warn(`TransportShipExecution: target ${this.targetID} not found`); + this.active = false; + return; + } + this.lastMove = ticks; this.mg = mg; this.pathFinder = PathFinder.Mini(mg, 10_000, false, 2); diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts index c84cc9823..09f806d8a 100644 --- a/src/core/execution/WarshipExecution.ts +++ b/src/core/execution/WarshipExecution.ts @@ -43,6 +43,11 @@ export class WarshipExecution implements Execution { ) {} init(mg: Game, ticks: number): void { + if (!mg.hasPlayer(this.playerID)) { + console.log(`WarshipExecution: player ${this.playerID} not found`); + this.active = false; + return; + } this.pathfinder = PathFinder.Mini(mg, 5000, false); this._owner = mg.player(this.playerID); this.mg = mg; diff --git a/src/core/execution/alliance/AllianceRequestExecution.ts b/src/core/execution/alliance/AllianceRequestExecution.ts index c5a20843a..21128d840 100644 --- a/src/core/execution/alliance/AllianceRequestExecution.ts +++ b/src/core/execution/alliance/AllianceRequestExecution.ts @@ -19,6 +19,21 @@ export class AllianceRequestExecution implements Execution { ) {} init(mg: Game, ticks: number): void { + if (!mg.hasPlayer(this.requestorID)) { + console.warn( + `AllianceRequestExecution requester ${this.requestorID} not found`, + ); + this.active = false; + return; + } + if (!mg.hasPlayer(this.recipientID)) { + console.warn( + `AllianceRequestExecution recipient ${this.recipientID} not found`, + ); + this.active = false; + return; + } + this.mg = mg; this.requestor = mg.player(this.requestorID); this.recipient = mg.player(this.recipientID); diff --git a/src/core/execution/alliance/AllianceRequestReplyExecution.ts b/src/core/execution/alliance/AllianceRequestReplyExecution.ts index 4250d476a..d0c0d4688 100644 --- a/src/core/execution/alliance/AllianceRequestReplyExecution.ts +++ b/src/core/execution/alliance/AllianceRequestReplyExecution.ts @@ -20,6 +20,20 @@ export class AllianceRequestReplyExecution implements Execution { ) {} init(mg: Game, ticks: number): void { + if (!mg.hasPlayer(this.requestorID)) { + console.warn( + `AllianceRequestReplyExecution requester ${this.requestorID} not found`, + ); + this.active = false; + return; + } + if (!mg.hasPlayer(this.recipientID)) { + console.warn( + `AllianceRequestReplyExecution recipient ${this.recipientID} not found`, + ); + this.active = false; + return; + } this.mg = mg; this.requestor = mg.player(this.requestorID); this.recipient = mg.player(this.recipientID); diff --git a/src/core/execution/alliance/BreakAllianceExecution.ts b/src/core/execution/alliance/BreakAllianceExecution.ts index 2beff3d8a..074e72ab7 100644 --- a/src/core/execution/alliance/BreakAllianceExecution.ts +++ b/src/core/execution/alliance/BreakAllianceExecution.ts @@ -19,6 +19,20 @@ export class BreakAllianceExecution implements Execution { ) {} init(mg: Game, ticks: number): void { + if (!mg.hasPlayer(this.requestorID)) { + console.warn( + `BreakAllianceExecution requester ${this.requestorID} not found`, + ); + this.active = false; + return; + } + if (!mg.hasPlayer(this.recipientID)) { + console.warn( + `BreakAllianceExecution: recipient ${this.recipientID} not found`, + ); + this.active = false; + return; + } this.requestor = mg.player(this.requestorID); this.recipient = mg.player(this.recipientID); this.mg = mg; diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts index f2d212932..c949ead5f 100644 --- a/src/server/GameManager.ts +++ b/src/server/GameManager.ts @@ -9,6 +9,7 @@ import { PseudoRandom } from "../core/PseudoRandom"; export class GameManager { private lastNewLobby: number = 0; + private mapsPlaylist: GameMapType[] = []; private games: GameServer[] = []; @@ -77,6 +78,31 @@ export class GameManager { } } + private getNextMap(): GameMapType { + if (this.mapsPlaylist.length > 0) { + return this.mapsPlaylist.shift(); + } + while (true) { + this.mapsPlaylist = Object.values(GameMapType); + this.mapsPlaylist.push(GameMapType.World); + this.mapsPlaylist.push(GameMapType.Europe); + this.random.shuffleArray(this.mapsPlaylist); + if (this.allNonConsecutive(this.mapsPlaylist)) { + return this.mapsPlaylist.shift(); + } + } + } + + private allNonConsecutive(maps: GameMapType[]): boolean { + // Check for consecutive duplicates in the maps array + for (let i = 0; i < maps.length - 1; i++) { + if (maps[i] === maps[i + 1]) { + return false; + } + } + return true; + } + tick() { const lobbies = this.gamesByPhase(GamePhase.Lobby); const active = this.gamesByPhase(GamePhase.Active); @@ -87,7 +113,7 @@ export class GameManager { this.lastNewLobby = now; lobbies.push( new GameServer(generateID(), now, true, this.config, { - gameMap: this.random.randElement(Object.values(GameMapType)), + gameMap: this.getNextMap(), gameType: GameType.Public, difficulty: Difficulty.Medium, disableBots: false, diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 1b77e274b..e7b2acafc 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -103,6 +103,19 @@ export class GameServer { const clientMsg: ClientMessage = ClientMessageSchema.parse( JSON.parse(message), ); + if (this.allClients.has(clientMsg.clientID)) { + const client = this.allClients.get(clientMsg.clientID); + if (client.persistentID != clientMsg.persistentID) { + console.warn( + `Client ID ${clientMsg.clientID} sent incorrect id ${clientMsg.persistentID}, does not match persistent id ${client.persistentID}`, + ); + return; + } + } + + // Clear out persistent id to make sure it doesn't get sent to other clients. + clientMsg.persistentID = null; + if (clientMsg.type == "intent") { if (clientMsg.gameID == this.id) { this.addIntent(clientMsg.intent);