mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-24 13:52:45 +00:00
Connection status monitoring
This commit is contained in:
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
@@ -1,8 +1,8 @@
|
||||
import allianceIcon from "../../../../resources/images/AllianceIcon.svg";
|
||||
import allianceRequestIcon from "../../../../resources/images/AllianceRequestIcon.svg";
|
||||
import crownIcon from "../../../../resources/images/CrownIcon.svg";
|
||||
import disconnectedIcon from "../../../../resources/images/DisconnectedIcon.svg";
|
||||
import embargoIcon from "../../../../resources/images/EmbargoIcon.svg";
|
||||
import idleIcon from "../../../../resources/images/IdleIcon.svg";
|
||||
import nukeRedIcon from "../../../../resources/images/NukeIconRed.svg";
|
||||
import nukeWhiteIcon from "../../../../resources/images/NukeIconWhite.svg";
|
||||
import shieldIcon from "../../../../resources/images/ShieldIconBlack.svg";
|
||||
@@ -39,7 +39,7 @@ export class NameLayer implements Layer {
|
||||
private renders: RenderInfo[] = [];
|
||||
private seenPlayers: Set<PlayerView> = new Set();
|
||||
private traitorIconImage: HTMLImageElement;
|
||||
private idleIconImage: HTMLImageElement;
|
||||
private disconnectedIconImage: HTMLImageElement;
|
||||
private allianceRequestIconImage: HTMLImageElement;
|
||||
private allianceIconImage: HTMLImageElement;
|
||||
private targetIconImage: HTMLImageElement;
|
||||
@@ -60,8 +60,8 @@ export class NameLayer implements Layer {
|
||||
) {
|
||||
this.traitorIconImage = new Image();
|
||||
this.traitorIconImage.src = traitorIcon;
|
||||
this.idleIconImage = new Image();
|
||||
this.idleIconImage.src = idleIcon;
|
||||
this.disconnectedIconImage = new Image();
|
||||
this.disconnectedIconImage.src = disconnectedIcon;
|
||||
this.allianceIconImage = new Image();
|
||||
this.allianceIconImage.src = allianceIcon;
|
||||
this.allianceRequestIconImage = new Image();
|
||||
@@ -353,16 +353,22 @@ export class NameLayer implements Layer {
|
||||
existingTraitor.remove();
|
||||
}
|
||||
|
||||
// Idle icon
|
||||
const existingIdle = iconsDiv.querySelector('[data-icon="idle"]');
|
||||
if (render.player.isIdle()) {
|
||||
if (!existingIdle) {
|
||||
// Disconnected icon
|
||||
const existingDisconnected = iconsDiv.querySelector(
|
||||
'[data-icon="disconnected"]',
|
||||
);
|
||||
if (render.player.isDisconnected()) {
|
||||
if (!existingDisconnected) {
|
||||
iconsDiv.appendChild(
|
||||
this.createIconElement(this.idleIconImage.src, iconSize, "idle"),
|
||||
this.createIconElement(
|
||||
this.disconnectedIconImage.src,
|
||||
iconSize,
|
||||
"disconnected",
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (existingIdle) {
|
||||
existingIdle.remove();
|
||||
} else if (existingDisconnected) {
|
||||
existingDisconnected.remove();
|
||||
}
|
||||
|
||||
// Alliance icon
|
||||
|
||||
+9
-7
@@ -34,7 +34,7 @@ export type Intent =
|
||||
| EmbargoIntent
|
||||
| QuickChatIntent
|
||||
| MoveWarshipIntent
|
||||
| MarkIdleIntent;
|
||||
| MarkDisconnectedIntent;
|
||||
|
||||
export type AttackIntent = z.infer<typeof AttackIntentSchema>;
|
||||
export type CancelAttackIntent = z.infer<typeof CancelAttackIntentSchema>;
|
||||
@@ -57,7 +57,9 @@ export type TargetTroopRatioIntent = z.infer<
|
||||
export type BuildUnitIntent = z.infer<typeof BuildUnitIntentSchema>;
|
||||
export type MoveWarshipIntent = z.infer<typeof MoveWarshipIntentSchema>;
|
||||
export type QuickChatIntent = z.infer<typeof QuickChatIntentSchema>;
|
||||
export type MarkIdleIntent = z.infer<typeof MarkIdleIntentSchema>;
|
||||
export type MarkDisconnectedIntent = z.infer<
|
||||
typeof MarkDisconnectedIntentSchema
|
||||
>;
|
||||
|
||||
export type Turn = z.infer<typeof TurnSchema>;
|
||||
export type GameConfig = z.infer<typeof GameConfigSchema>;
|
||||
@@ -168,7 +170,7 @@ const BaseIntentSchema = z.object({
|
||||
"attack",
|
||||
"cancel_attack",
|
||||
"spawn",
|
||||
"mark_idle",
|
||||
"mark_disconnected",
|
||||
"boat",
|
||||
"cancel_boat",
|
||||
"name",
|
||||
@@ -293,16 +295,16 @@ export const QuickChatIntentSchema = BaseIntentSchema.extend({
|
||||
variables: z.record(SafeString).optional(),
|
||||
});
|
||||
|
||||
export const MarkIdleIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("mark_idle"),
|
||||
isIdle: z.boolean(),
|
||||
export const MarkDisconnectedIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("mark_disconnected"),
|
||||
isDisconnected: z.boolean(),
|
||||
});
|
||||
|
||||
const IntentSchema = z.union([
|
||||
AttackIntentSchema,
|
||||
CancelAttackIntentSchema,
|
||||
SpawnIntentSchema,
|
||||
MarkIdleIntentSchema,
|
||||
MarkDisconnectedIntentSchema,
|
||||
BoatAttackIntentSchema,
|
||||
CancelBoatIntentSchema,
|
||||
AllianceRequestIntentSchema,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { DonateTroopsExecution } from "./DonateTroopExecution";
|
||||
import { EmbargoExecution } from "./EmbargoExecution";
|
||||
import { EmojiExecution } from "./EmojiExecution";
|
||||
import { FakeHumanExecution } from "./FakeHumanExecution";
|
||||
import { MarkIdleExecution } from "./MarkIdleExecution";
|
||||
import { MarkDisconnectedExecution } from "./MarkDisconnectedExecution";
|
||||
import { MoveWarshipExecution } from "./MoveWarshipExecution";
|
||||
import { NoOpExecution } from "./NoOpExecution";
|
||||
import { QuickChatExecution } from "./QuickChatExecution";
|
||||
@@ -121,8 +121,8 @@ export class Executor {
|
||||
intent.quickChatKey,
|
||||
intent.variables ?? {},
|
||||
);
|
||||
case "mark_idle":
|
||||
return new MarkIdleExecution(playerID, intent.isIdle);
|
||||
case "mark_disconnected":
|
||||
return new MarkDisconnectedExecution(playerID, intent.isDisconnected);
|
||||
default:
|
||||
throw new Error(`intent type ${intent} not found`);
|
||||
}
|
||||
|
||||
+5
-5
@@ -1,18 +1,18 @@
|
||||
import { Execution, Game, Player, PlayerID } from "../game/Game";
|
||||
|
||||
export class MarkIdleExecution implements Execution {
|
||||
export class MarkDisconnectedExecution implements Execution {
|
||||
private player: Player;
|
||||
private active: boolean = true;
|
||||
|
||||
constructor(
|
||||
private playerID: PlayerID,
|
||||
private isIdle: boolean,
|
||||
private isDisconnected: boolean,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this.playerID)) {
|
||||
console.warn(
|
||||
`MarkIdleExecution: player ${this.playerID} not found in game`,
|
||||
`MarkDisconnectedExecution: player ${this.playerID} not found in game`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -21,7 +21,7 @@ export class MarkIdleExecution implements Execution {
|
||||
this.player = mg.player(this.playerID);
|
||||
if (!this.player) {
|
||||
console.warn(
|
||||
`MarkIdleExecution: failed to retrieve player ${this.playerID}`,
|
||||
`MarkDisconnectedExecution: failed to retrieve player ${this.playerID}`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -29,7 +29,7 @@ export class MarkIdleExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
this.player.markIdle(this.isIdle);
|
||||
this.player.markDisconnected(this.isDisconnected);
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
@@ -433,8 +433,8 @@ export interface Player {
|
||||
largestClusterBoundingBox: { min: Cell; max: Cell } | null;
|
||||
lastTileChange(): Tick;
|
||||
|
||||
isIdle(): boolean;
|
||||
markIdle(isIdle: boolean): void;
|
||||
isDisconnected(): boolean;
|
||||
markDisconnected(isDisconnected: boolean): void;
|
||||
|
||||
hasSpawned(): boolean;
|
||||
setHasSpawned(hasSpawned: boolean): void;
|
||||
|
||||
@@ -102,7 +102,7 @@ export interface PlayerUpdate {
|
||||
smallID: number;
|
||||
playerType: PlayerType;
|
||||
isAlive: boolean;
|
||||
isIdle: boolean;
|
||||
isDisconnected: boolean;
|
||||
tilesOwned: number;
|
||||
gold: number;
|
||||
population: number;
|
||||
|
||||
@@ -292,8 +292,8 @@ export class PlayerView {
|
||||
hasSpawned(): boolean {
|
||||
return this.data.hasSpawned;
|
||||
}
|
||||
isIdle(): boolean {
|
||||
return this.data.isIdle;
|
||||
isDisconnected(): boolean {
|
||||
return this.data.isDisconnected;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ export class PlayerImpl implements Player {
|
||||
public _outgoingLandAttacks: Attack[] = [];
|
||||
|
||||
private _hasSpawned = false;
|
||||
private _isIdle = false;
|
||||
private _isDisconnected = false;
|
||||
|
||||
constructor(
|
||||
private mg: GameImpl,
|
||||
@@ -137,7 +137,7 @@ export class PlayerImpl implements Player {
|
||||
smallID: this.smallID(),
|
||||
playerType: this.type(),
|
||||
isAlive: this.isAlive(),
|
||||
isIdle: this.isIdle(),
|
||||
isDisconnected: this.isDisconnected(),
|
||||
tilesOwned: this.numTilesOwned(),
|
||||
gold: Number(this._gold),
|
||||
population: this.population(),
|
||||
@@ -927,12 +927,12 @@ export class PlayerImpl implements Player {
|
||||
return this._lastTileChange;
|
||||
}
|
||||
|
||||
isIdle(): boolean {
|
||||
return this._isIdle;
|
||||
isDisconnected(): boolean {
|
||||
return this._isDisconnected;
|
||||
}
|
||||
|
||||
markIdle(isIdle: boolean): void {
|
||||
this._isIdle = isIdle;
|
||||
markDisconnected(isDisconnected: boolean): void {
|
||||
this._isDisconnected = isDisconnected;
|
||||
}
|
||||
|
||||
hash(): number {
|
||||
|
||||
@@ -5,8 +5,7 @@ import { ClientID } from "../core/Schemas";
|
||||
|
||||
export class Client {
|
||||
public lastPing: number = Date.now();
|
||||
public lastAction: number = Date.now();
|
||||
public isIdle: boolean = false;
|
||||
public isDisconnected: boolean = false;
|
||||
|
||||
public hashes: Map<Tick, number> = new Map();
|
||||
|
||||
|
||||
+14
-18
@@ -35,7 +35,7 @@ export class GameServer {
|
||||
|
||||
private maxGameDuration = 3 * 60 * 60 * 1000; // 3 hours
|
||||
|
||||
private idleTimeout = 1 * 60 * 1000; // 1 minute
|
||||
private disconnectedTimeout = 1 * 30 * 1000; // 30 seconds
|
||||
|
||||
private turns: Turn[] = [];
|
||||
private intents: Intent[] = [];
|
||||
@@ -167,8 +167,7 @@ export class GameServer {
|
||||
return;
|
||||
}
|
||||
|
||||
client.isIdle = existing.isIdle;
|
||||
client.lastAction = existing.lastAction;
|
||||
client.isDisconnected = existing.isDisconnected;
|
||||
client.lastPing = existing.lastPing;
|
||||
|
||||
existing.ws.removeAllListeners("message");
|
||||
@@ -198,7 +197,6 @@ export class GameServer {
|
||||
);
|
||||
return;
|
||||
}
|
||||
client.lastAction = Date.now();
|
||||
this.addIntent(clientMsg.intent);
|
||||
}
|
||||
if (clientMsg.type === "ping") {
|
||||
@@ -361,7 +359,7 @@ export class GameServer {
|
||||
this.intents = [];
|
||||
|
||||
this.handleSynchronization();
|
||||
this.checkIdleStatus();
|
||||
this.checkDisconnectedStatus();
|
||||
|
||||
let msg = "";
|
||||
try {
|
||||
@@ -542,7 +540,7 @@ export class GameServer {
|
||||
}
|
||||
}
|
||||
|
||||
private checkIdleStatus() {
|
||||
private checkDisconnectedStatus() {
|
||||
if (this.turns.length % 5 !== 0) {
|
||||
return;
|
||||
}
|
||||
@@ -550,27 +548,25 @@ export class GameServer {
|
||||
const now = Date.now();
|
||||
for (const [clientID, client] of this.allClients) {
|
||||
if (
|
||||
client.isIdle === false &&
|
||||
now - client.lastPing > this.idleTimeout &&
|
||||
now - client.lastAction > this.idleTimeout
|
||||
client.isDisconnected === false &&
|
||||
now - client.lastPing > this.disconnectedTimeout
|
||||
) {
|
||||
this.markClientIdle(client, true);
|
||||
this.markClientDisconnected(client, true);
|
||||
} else if (
|
||||
client.isIdle &&
|
||||
now - client.lastPing < this.idleTimeout &&
|
||||
now - client.lastAction < this.idleTimeout
|
||||
client.isDisconnected &&
|
||||
now - client.lastPing < this.disconnectedTimeout
|
||||
) {
|
||||
this.markClientIdle(client, false);
|
||||
this.markClientDisconnected(client, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private markClientIdle(client: Client, isIdle: boolean) {
|
||||
client.isIdle = isIdle;
|
||||
private markClientDisconnected(client: Client, isDisconnected: boolean) {
|
||||
client.isDisconnected = isDisconnected;
|
||||
this.addIntent({
|
||||
type: "mark_idle",
|
||||
type: "mark_disconnected",
|
||||
clientID: client.clientID,
|
||||
isIdle: isIdle,
|
||||
isDisconnected: isDisconnected,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
import { MarkDisconnectedExecution } from "../src/core/execution/MarkDisconnectedExecution";
|
||||
import { SpawnExecution } from "../src/core/execution/SpawnExecution";
|
||||
import { Game, Player, PlayerInfo, PlayerType } from "../src/core/game/Game";
|
||||
import { setup } from "./util/Setup";
|
||||
import { executeTicks } from "./util/utils";
|
||||
|
||||
let game: Game;
|
||||
let player1: Player;
|
||||
let player2: Player;
|
||||
|
||||
describe("Disconnected", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup("Plains", {
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
});
|
||||
|
||||
const player1Info = new PlayerInfo(
|
||||
"us",
|
||||
"Active Player",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"player1_id",
|
||||
);
|
||||
|
||||
const player2Info = new PlayerInfo(
|
||||
"fr",
|
||||
"Disconnected Player",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"player2_id",
|
||||
);
|
||||
|
||||
player1 = game.addPlayer(player1Info);
|
||||
player2 = game.addPlayer(player2Info);
|
||||
|
||||
game.addExecution(
|
||||
new SpawnExecution(player1Info, game.ref(1, 1)),
|
||||
new SpawnExecution(player2Info, game.ref(7, 7)),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
});
|
||||
|
||||
describe("Player disconnected state", () => {
|
||||
test("should initialize players as not disconnected", () => {
|
||||
expect(player1.isDisconnected()).toBe(false);
|
||||
expect(player2.isDisconnected()).toBe(false);
|
||||
});
|
||||
|
||||
test("should mark player as disconnected", () => {
|
||||
player1.markDisconnected(true);
|
||||
expect(player1.isDisconnected()).toBe(true);
|
||||
});
|
||||
|
||||
test("should mark player as not disconnected", () => {
|
||||
player1.markDisconnected(true);
|
||||
expect(player1.isDisconnected()).toBe(true);
|
||||
|
||||
player1.markDisconnected(false);
|
||||
expect(player1.isDisconnected()).toBe(false);
|
||||
});
|
||||
|
||||
test("should include disconnected state in player update", () => {
|
||||
player1.markDisconnected(true);
|
||||
const update = player1.toUpdate();
|
||||
expect(update.isDisconnected).toBe(true);
|
||||
|
||||
player1.markDisconnected(false);
|
||||
const update2 = player1.toUpdate();
|
||||
expect(update2.isDisconnected).toBe(false);
|
||||
});
|
||||
|
||||
test("should maintain disconnected state independently for different players", () => {
|
||||
player1.markDisconnected(true);
|
||||
player2.markDisconnected(false);
|
||||
|
||||
expect(player1.isDisconnected()).toBe(true);
|
||||
expect(player2.isDisconnected()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MarkDisconnectedExecution", () => {
|
||||
test("should mark player as disconnected when executed", () => {
|
||||
const execution = new MarkDisconnectedExecution(player1.id(), true);
|
||||
game.addExecution(execution);
|
||||
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(player1.isDisconnected()).toBe(true);
|
||||
expect(execution.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
test("should mark player as not disconnected when executed", () => {
|
||||
// First mark as disconnected directly
|
||||
player1.markDisconnected(true);
|
||||
expect(player1.isDisconnected()).toBe(true);
|
||||
|
||||
// Then mark as not disconnected via execution
|
||||
const execution = new MarkDisconnectedExecution(player1.id(), false);
|
||||
game.addExecution(execution);
|
||||
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(player1.isDisconnected()).toBe(false);
|
||||
expect(execution.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
test("should handle multiple players with different disconnected states", () => {
|
||||
const execution1 = new MarkDisconnectedExecution(player1.id(), true);
|
||||
const execution2 = new MarkDisconnectedExecution(player2.id(), false);
|
||||
|
||||
game.addExecution(execution1, execution2);
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(player1.isDisconnected()).toBe(true);
|
||||
expect(player2.isDisconnected()).toBe(false);
|
||||
expect(execution1.isActive()).toBe(false);
|
||||
expect(execution2.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
test("should handle invalid player ID gracefully", () => {
|
||||
const execution = new MarkDisconnectedExecution(
|
||||
"invalid_player_id",
|
||||
true,
|
||||
);
|
||||
game.addExecution(execution);
|
||||
|
||||
// Should not throw and should deactivate
|
||||
expect(() => game.executeNextTick()).not.toThrow();
|
||||
expect(execution.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
test("should not be active during spawn phase", () => {
|
||||
const execution = new MarkDisconnectedExecution(player1.id(), true);
|
||||
expect(execution.activeDuringSpawnPhase()).toBe(false);
|
||||
});
|
||||
|
||||
test("should handle rapid disconnected state changes", () => {
|
||||
// Mark disconnected
|
||||
const execution1 = new MarkDisconnectedExecution(player1.id(), true);
|
||||
game.addExecution(execution1);
|
||||
executeTicks(game, 2);
|
||||
expect(player1.isDisconnected()).toBe(true);
|
||||
|
||||
// Mark not disconnected
|
||||
const execution2 = new MarkDisconnectedExecution(player1.id(), false);
|
||||
game.addExecution(execution2);
|
||||
executeTicks(game, 2);
|
||||
expect(player1.isDisconnected()).toBe(false);
|
||||
|
||||
// Mark disconnected again
|
||||
const execution3 = new MarkDisconnectedExecution(player1.id(), true);
|
||||
game.addExecution(execution3);
|
||||
executeTicks(game, 2);
|
||||
expect(player1.isDisconnected()).toBe(true);
|
||||
});
|
||||
|
||||
test("should execute properly with other executions in same tick", () => {
|
||||
const markDisconnectedExecution = new MarkDisconnectedExecution(
|
||||
player1.id(),
|
||||
true,
|
||||
);
|
||||
const markDisconnectedExecution2 = new MarkDisconnectedExecution(
|
||||
player2.id(),
|
||||
false,
|
||||
);
|
||||
|
||||
game.addExecution(markDisconnectedExecution, markDisconnectedExecution2);
|
||||
|
||||
// Execute multiple ticks to ensure all executions complete
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(player1.isDisconnected()).toBe(true);
|
||||
expect(player2.isDisconnected()).toBe(false);
|
||||
expect(markDisconnectedExecution.isActive()).toBe(false);
|
||||
expect(markDisconnectedExecution2.isActive()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Disconnected state persistence", () => {
|
||||
test("should maintain disconnected state across game ticks", () => {
|
||||
player1.markDisconnected(true);
|
||||
|
||||
// Execute several ticks
|
||||
executeTicks(game, 5);
|
||||
|
||||
// Disconnected state should persist
|
||||
expect(player1.isDisconnected()).toBe(true);
|
||||
});
|
||||
|
||||
test("should maintain disconnected state in player updates", () => {
|
||||
player1.markDisconnected(true);
|
||||
|
||||
// Execute some ticks and check update still shows disconnected
|
||||
executeTicks(game, 3);
|
||||
|
||||
const update = player1.toUpdate();
|
||||
expect(update.isDisconnected).toBe(true);
|
||||
});
|
||||
|
||||
test("should handle execution during different game phases", () => {
|
||||
// Test that disconnected execution works outside spawn phase
|
||||
expect(game.inSpawnPhase()).toBe(false);
|
||||
|
||||
const execution = new MarkDisconnectedExecution(player1.id(), true);
|
||||
game.addExecution(execution);
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(player1.isDisconnected()).toBe(true);
|
||||
expect(execution.isActive()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge cases", () => {
|
||||
test("should handle marking same disconnected state multiple times", () => {
|
||||
// Mark disconnected multiple times
|
||||
player1.markDisconnected(true);
|
||||
player1.markDisconnected(true);
|
||||
player1.markDisconnected(true);
|
||||
|
||||
expect(player1.isDisconnected()).toBe(true);
|
||||
|
||||
// Mark not disconnected multiple times
|
||||
player1.markDisconnected(false);
|
||||
player1.markDisconnected(false);
|
||||
player1.markDisconnected(false);
|
||||
|
||||
expect(player1.isDisconnected()).toBe(false);
|
||||
});
|
||||
|
||||
test("should handle execution with same disconnected state", () => {
|
||||
// Start with player disconnected
|
||||
player1.markDisconnected(true);
|
||||
expect(player1.isDisconnected()).toBe(true);
|
||||
|
||||
// Execute with same disconnected state
|
||||
const execution = new MarkDisconnectedExecution(player1.id(), true);
|
||||
game.addExecution(execution);
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(player1.isDisconnected()).toBe(true);
|
||||
expect(execution.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
test("should handle missing player during execution init", () => {
|
||||
const execution = new MarkDisconnectedExecution(
|
||||
"nonexistent_player",
|
||||
true,
|
||||
);
|
||||
|
||||
// Mock console.warn to verify it's called
|
||||
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
||||
|
||||
game.addExecution(execution);
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(execution.isActive()).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"MarkDisconnectedExecution: player nonexistent_player not found",
|
||||
),
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("should handle multiple executions for same player", () => {
|
||||
const execution1 = new MarkDisconnectedExecution(player1.id(), true);
|
||||
const execution2 = new MarkDisconnectedExecution(player1.id(), false);
|
||||
|
||||
game.addExecution(execution1, execution2);
|
||||
executeTicks(game, 2);
|
||||
|
||||
// Last execution should win
|
||||
expect(player1.isDisconnected()).toBe(false);
|
||||
expect(execution1.isActive()).toBe(false);
|
||||
expect(execution2.isActive()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,271 +0,0 @@
|
||||
import { MarkIdleExecution } from "../src/core/execution/MarkIdleExecution";
|
||||
import { SpawnExecution } from "../src/core/execution/SpawnExecution";
|
||||
import { Game, Player, PlayerInfo, PlayerType } from "../src/core/game/Game";
|
||||
import { setup } from "./util/Setup";
|
||||
import { executeTicks } from "./util/utils";
|
||||
|
||||
let game: Game;
|
||||
let player1: Player;
|
||||
let player2: Player;
|
||||
|
||||
describe("Idle", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup("Plains", {
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
});
|
||||
|
||||
const player1Info = new PlayerInfo(
|
||||
"us",
|
||||
"Active Player",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"player1_id",
|
||||
);
|
||||
|
||||
const player2Info = new PlayerInfo(
|
||||
"fr",
|
||||
"Idle Player",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"player2_id",
|
||||
);
|
||||
|
||||
player1 = game.addPlayer(player1Info);
|
||||
player2 = game.addPlayer(player2Info);
|
||||
|
||||
game.addExecution(
|
||||
new SpawnExecution(player1Info, game.ref(1, 1)),
|
||||
new SpawnExecution(player2Info, game.ref(7, 7)),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
});
|
||||
|
||||
describe("Player idle state", () => {
|
||||
test("should initialize players as not idle", () => {
|
||||
expect(player1.isIdle()).toBe(false);
|
||||
expect(player2.isIdle()).toBe(false);
|
||||
});
|
||||
|
||||
test("should mark player as idle", () => {
|
||||
player1.markIdle(true);
|
||||
expect(player1.isIdle()).toBe(true);
|
||||
});
|
||||
|
||||
test("should mark player as not idle", () => {
|
||||
player1.markIdle(true);
|
||||
expect(player1.isIdle()).toBe(true);
|
||||
|
||||
player1.markIdle(false);
|
||||
expect(player1.isIdle()).toBe(false);
|
||||
});
|
||||
|
||||
test("should include idle state in player update", () => {
|
||||
player1.markIdle(true);
|
||||
const update = player1.toUpdate();
|
||||
expect(update.isIdle).toBe(true);
|
||||
|
||||
player1.markIdle(false);
|
||||
const update2 = player1.toUpdate();
|
||||
expect(update2.isIdle).toBe(false);
|
||||
});
|
||||
|
||||
test("should maintain idle state independently for different players", () => {
|
||||
player1.markIdle(true);
|
||||
player2.markIdle(false);
|
||||
|
||||
expect(player1.isIdle()).toBe(true);
|
||||
expect(player2.isIdle()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MarkIdleExecution", () => {
|
||||
test("should mark player as idle when executed", () => {
|
||||
const execution = new MarkIdleExecution(player1.id(), true);
|
||||
game.addExecution(execution);
|
||||
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(player1.isIdle()).toBe(true);
|
||||
expect(execution.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
test("should mark player as not idle when executed", () => {
|
||||
// First mark as idle directly
|
||||
player1.markIdle(true);
|
||||
expect(player1.isIdle()).toBe(true);
|
||||
|
||||
// Then mark as not idle via execution
|
||||
const execution = new MarkIdleExecution(player1.id(), false);
|
||||
game.addExecution(execution);
|
||||
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(player1.isIdle()).toBe(false);
|
||||
expect(execution.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
test("should handle multiple players with different idle states", () => {
|
||||
const execution1 = new MarkIdleExecution(player1.id(), true);
|
||||
const execution2 = new MarkIdleExecution(player2.id(), false);
|
||||
|
||||
game.addExecution(execution1, execution2);
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(player1.isIdle()).toBe(true);
|
||||
expect(player2.isIdle()).toBe(false);
|
||||
expect(execution1.isActive()).toBe(false);
|
||||
expect(execution2.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
test("should handle invalid player ID gracefully", () => {
|
||||
const execution = new MarkIdleExecution("invalid_player_id", true);
|
||||
game.addExecution(execution);
|
||||
|
||||
// Should not throw and should deactivate
|
||||
expect(() => game.executeNextTick()).not.toThrow();
|
||||
expect(execution.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
test("should not be active during spawn phase", () => {
|
||||
const execution = new MarkIdleExecution(player1.id(), true);
|
||||
expect(execution.activeDuringSpawnPhase()).toBe(false);
|
||||
});
|
||||
|
||||
test("should handle rapid idle state changes", () => {
|
||||
// Mark idle
|
||||
const execution1 = new MarkIdleExecution(player1.id(), true);
|
||||
game.addExecution(execution1);
|
||||
executeTicks(game, 2);
|
||||
expect(player1.isIdle()).toBe(true);
|
||||
|
||||
// Mark not idle
|
||||
const execution2 = new MarkIdleExecution(player1.id(), false);
|
||||
game.addExecution(execution2);
|
||||
executeTicks(game, 2);
|
||||
expect(player1.isIdle()).toBe(false);
|
||||
|
||||
// Mark idle again
|
||||
const execution3 = new MarkIdleExecution(player1.id(), true);
|
||||
game.addExecution(execution3);
|
||||
executeTicks(game, 2);
|
||||
expect(player1.isIdle()).toBe(true);
|
||||
});
|
||||
|
||||
test("should execute properly with other executions in same tick", () => {
|
||||
const markIdleExecution = new MarkIdleExecution(player1.id(), true);
|
||||
const markIdleExecution2 = new MarkIdleExecution(player2.id(), false);
|
||||
|
||||
game.addExecution(markIdleExecution, markIdleExecution2);
|
||||
|
||||
// Execute multiple ticks to ensure all executions complete
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(player1.isIdle()).toBe(true);
|
||||
expect(player2.isIdle()).toBe(false);
|
||||
expect(markIdleExecution.isActive()).toBe(false);
|
||||
expect(markIdleExecution2.isActive()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Idle state persistence", () => {
|
||||
test("should maintain idle state across game ticks", () => {
|
||||
player1.markIdle(true);
|
||||
|
||||
// Execute several ticks
|
||||
executeTicks(game, 5);
|
||||
|
||||
// Idle state should persist
|
||||
expect(player1.isIdle()).toBe(true);
|
||||
});
|
||||
|
||||
test("should maintain idle state in player updates", () => {
|
||||
player1.markIdle(true);
|
||||
|
||||
// Execute some ticks and check update still shows idle
|
||||
executeTicks(game, 3);
|
||||
|
||||
const update = player1.toUpdate();
|
||||
expect(update.isIdle).toBe(true);
|
||||
});
|
||||
|
||||
test("should handle execution during different game phases", () => {
|
||||
// Test that idle execution works outside spawn phase
|
||||
expect(game.inSpawnPhase()).toBe(false);
|
||||
|
||||
const execution = new MarkIdleExecution(player1.id(), true);
|
||||
game.addExecution(execution);
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(player1.isIdle()).toBe(true);
|
||||
expect(execution.isActive()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge cases", () => {
|
||||
test("should handle marking same idle state multiple times", () => {
|
||||
// Mark idle multiple times
|
||||
player1.markIdle(true);
|
||||
player1.markIdle(true);
|
||||
player1.markIdle(true);
|
||||
|
||||
expect(player1.isIdle()).toBe(true);
|
||||
|
||||
// Mark not idle multiple times
|
||||
player1.markIdle(false);
|
||||
player1.markIdle(false);
|
||||
player1.markIdle(false);
|
||||
|
||||
expect(player1.isIdle()).toBe(false);
|
||||
});
|
||||
|
||||
test("should handle execution with same idle state", () => {
|
||||
// Start with player idle
|
||||
player1.markIdle(true);
|
||||
expect(player1.isIdle()).toBe(true);
|
||||
|
||||
// Execute with same idle state
|
||||
const execution = new MarkIdleExecution(player1.id(), true);
|
||||
game.addExecution(execution);
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(player1.isIdle()).toBe(true);
|
||||
expect(execution.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
test("should handle missing player during execution init", () => {
|
||||
const execution = new MarkIdleExecution("nonexistent_player", true);
|
||||
|
||||
// Mock console.warn to verify it's called
|
||||
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
||||
|
||||
game.addExecution(execution);
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(execution.isActive()).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"MarkIdleExecution: player nonexistent_player not found",
|
||||
),
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("should handle multiple executions for same player", () => {
|
||||
const execution1 = new MarkIdleExecution(player1.id(), true);
|
||||
const execution2 = new MarkIdleExecution(player1.id(), false);
|
||||
|
||||
game.addExecution(execution1, execution2);
|
||||
executeTicks(game, 2);
|
||||
|
||||
// Last execution should win
|
||||
expect(player1.isIdle()).toBe(false);
|
||||
expect(execution1.isActive()).toBe(false);
|
||||
expect(execution2.isActive()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user