strictNullChecks

This commit is contained in:
Scott Anderson
2025-04-06 20:16:37 -04:00
parent aac1cf0754
commit 3b07f78e97
82 changed files with 582 additions and 423 deletions
+18 -6
View File
@@ -61,7 +61,7 @@ export function joinLobby(
);
const userSettings: UserSettings = new UserSettings();
startGame(lobbyConfig.gameID, lobbyConfig.gameStartInfo?.config);
startGame(lobbyConfig.gameID, lobbyConfig.gameStartInfo?.config ?? {});
const transport = new Transport(lobbyConfig, eventBus);
@@ -107,6 +107,9 @@ export async function createClientGame(
userSettings: UserSettings,
terrainLoad: Promise<TerrainMapData> | null,
): Promise<ClientGameRunner> {
if (typeof lobbyConfig.gameStartInfo === "undefined") {
throw new Error("missing gameStartInfo");
}
const config = await getConfig(
lobbyConfig.gameStartInfo.config,
userSettings,
@@ -198,6 +201,9 @@ export class ClientGameRunner {
winner = update.winner as Team;
}
if (typeof this.lobby.gameStartInfo === "undefined") {
throw new Error("missing gameStartInfo");
}
const record = createGameRecord(
this.lobby.gameStartInfo.gameID,
this.lobby.gameStartInfo,
@@ -229,16 +235,20 @@ export class ClientGameRunner {
this.renderer.initialize();
this.input.initialize();
this.worker.start((gu: GameUpdateViewData | ErrorUpdate) => {
if (typeof this.lobby.gameStartInfo === "undefined") {
throw new Error("missing gameStartInfo");
}
if ("errMsg" in gu) {
showErrorModal(
gu.errMsg,
gu.stack,
gu.stack ?? "missing",
this.lobby.gameStartInfo.gameID,
this.lobby.clientID,
);
this.stop(true);
return;
}
if (gu.updates === null) return;
gu.updates[GameUpdateType.Hash].forEach((hu: HashUpdate) => {
this.eventBus.emit(new SendHashEvent(hu.tick, hu.hash));
});
@@ -282,6 +292,9 @@ export class ClientGameRunner {
}
}
if (message.type == "desync") {
if (typeof this.lobby.gameStartInfo === "undefined") {
throw new Error("missing gameStartInfo");
}
showErrorModal(
`desync from server: ${JSON.stringify(message)}`,
"",
@@ -344,10 +357,9 @@ export class ClientGameRunner {
return;
}
if (this.myPlayer == null) {
this.myPlayer = this.gameView.playerByClientID(this.lobby.clientID);
if (this.myPlayer == null) {
return;
}
const myPlayer = this.gameView.playerByClientID(this.lobby.clientID);
if (myPlayer === null) return;
this.myPlayer = myPlayer;
}
this.myPlayer.actions(tile).then((actions) => {
console.log(`got actions: ${JSON.stringify(actions)}`);
+2 -2
View File
@@ -33,7 +33,7 @@ export class HostLobbyModal extends LitElement {
@state() private players: string[] = [];
@state() private useRandomMap: boolean = false;
private playersInterval = null;
private playersInterval: NodeJS.Timeout | null = null;
// Add a new timer for debouncing bot changes
private botsUpdateTimer: number | null = null;
@@ -493,7 +493,7 @@ export class HostLobbyModal extends LitElement {
.then((response) => response.json())
.then((data: GameInfo) => {
console.log(`got game info response: ${JSON.stringify(data)}`);
this.players = data.clients.map((p) => p.username);
this.players = data.clients?.map((p) => p.username) ?? [];
});
}
}
+4 -2
View File
@@ -99,7 +99,7 @@ export class InputHandler {
private alternateView = false;
private moveInterval: NodeJS.Timeout = null;
private moveInterval: NodeJS.Timeout | null = null;
private activeKeys = new Set<string>();
private readonly PAN_SPEED = 5;
@@ -392,7 +392,9 @@ export class InputHandler {
}
destroy() {
clearInterval(this.moveInterval);
if (this.moveInterval !== null) {
clearInterval(this.moveInterval);
}
this.activeKeys.clear();
}
}
+3 -3
View File
@@ -19,7 +19,7 @@ export class JoinPrivateLobbyModal extends LitElement {
@state() private hasJoined = false;
@state() private players: string[] = [];
private playersInterval = null;
private playersInterval: NodeJS.Timeout | null = null;
render() {
return html`
@@ -98,7 +98,7 @@ export class JoinPrivateLobbyModal extends LitElement {
}
public close() {
this.lobbyIdInput.value = null;
this.lobbyIdInput.value = "";
this.modalEl?.close();
if (this.playersInterval) {
clearInterval(this.playersInterval);
@@ -263,7 +263,7 @@ export class JoinPrivateLobbyModal extends LitElement {
)
.then((response) => response.json())
.then((data: GameInfo) => {
this.players = data.clients.map((p) => p.username);
this.players = data.clients?.map((p) => p.username) ?? [];
})
.catch((error) => {
consolex.error("Error polling players:", error);
+2 -2
View File
@@ -3,7 +3,7 @@ import { GameConfig, GameID, GameRecord } from "../core/Schemas";
export interface LocalStatsData {
[key: GameID]: {
lobby: GameConfig;
lobby: Partial<GameConfig>;
// Only once the game is over
gameRecord?: GameRecord;
};
@@ -26,7 +26,7 @@ function save(stats: LocalStatsData) {
// The user can quit the game anytime so better save the lobby as soon as the
// game starts.
export function startGame(id: GameID, lobby: GameConfig) {
export function startGame(id: GameID, lobby: Partial<GameConfig>) {
if (typeof localStorage === "undefined") {
return;
}
+13 -4
View File
@@ -20,11 +20,11 @@ export class LocalServer {
private intents: Intent[] = [];
private startedAt: number;
private endTurnIntervalID;
private endTurnIntervalID: NodeJS.Timeout;
private paused = false;
private winner: ClientSendWinnerMessage = null;
private winner: ClientSendWinnerMessage | null = null;
private allPlayersStats: AllPlayersStats = {};
constructor(
@@ -46,6 +46,9 @@ export class LocalServer {
this.turns = decompressGameRecord(this.lobbyConfig.gameRecord).turns;
console.log(`loaded turns: ${JSON.stringify(this.turns)}`);
}
if (typeof this.lobbyConfig.gameStartInfo === "undefined") {
throw new Error("missing gameStartInfo");
}
this.clientMessage(
ServerStartGameMessageSchema.parse({
type: "start",
@@ -125,6 +128,9 @@ export class LocalServer {
if (this.paused) {
return;
}
if (typeof this.lobbyConfig.gameStartInfo === "undefined") {
throw new Error("missing gameStartInfo");
}
const pastTurn: Turn = {
turnNumber: this.turns.length,
gameID: this.lobbyConfig.gameStartInfo.gameID,
@@ -149,6 +155,9 @@ export class LocalServer {
clientID: this.lobbyConfig.clientID,
},
];
if (typeof this.lobbyConfig.gameStartInfo === "undefined") {
throw new Error("missing gameStartInfo");
}
const record = createGameRecord(
this.lobbyConfig.gameStartInfo.gameID,
this.lobbyConfig.gameStartInfo,
@@ -156,8 +165,8 @@ export class LocalServer {
this.turns,
this.startedAt,
Date.now(),
this.winner?.winner,
this.winner?.winnerType,
this.winner?.winner ?? null,
this.winner?.winnerType ?? null,
this.allPlayersStats,
);
if (!saveFullGame) {
+28 -21
View File
@@ -40,7 +40,7 @@ export interface JoinLobbyEvent {
}
class Client {
private gameStop: () => void;
private gameStop: (() => void) | null;
private usernameInput: UsernameInput | null = null;
private flagInput: FlagInput | null = null;
@@ -106,15 +106,19 @@ class Client {
"single-player-modal",
) as SinglePlayerModal;
spModal instanceof SinglePlayerModal;
document.getElementById("single-player").addEventListener("click", () => {
if (this.usernameInput.isValid()) {
const singlePlayer = document.getElementById("single-player");
if (singlePlayer === null) throw new Error("Missing single-player");
singlePlayer.addEventListener("click", () => {
if (this.usernameInput?.isValid()) {
spModal.open();
}
});
const hlpModal = document.querySelector("help-modal") as HelpModal;
hlpModal instanceof HelpModal;
document.getElementById("help-button").addEventListener("click", () => {
const helpButton = document.getElementById("help-button");
if (helpButton === null) throw new Error("Missing help-button");
helpButton.addEventListener("click", () => {
hlpModal.open();
});
@@ -122,26 +126,29 @@ class Client {
"host-lobby-modal",
) as HostPrivateLobbyModal;
hostModal instanceof HostPrivateLobbyModal;
document
.getElementById("host-lobby-button")
.addEventListener("click", () => {
if (this.usernameInput.isValid()) {
hostModal.open();
this.publicLobby.leaveLobby();
}
});
const hostLobbyButton = document.getElementById("host-lobby-button");
if (hostLobbyButton === null) throw new Error("Missing host-lobby-button");
hostLobbyButton.addEventListener("click", () => {
if (this.usernameInput?.isValid()) {
hostModal.open();
this.publicLobby.leaveLobby();
}
});
this.joinModal = document.querySelector(
"join-private-lobby-modal",
) as JoinPrivateLobbyModal;
this.joinModal instanceof JoinPrivateLobbyModal;
document
.getElementById("join-private-lobby-button")
.addEventListener("click", () => {
if (this.usernameInput.isValid()) {
this.joinModal.open();
}
});
const joinPrivateLobbyButton = document.getElementById(
"join-private-lobby-button",
);
if (joinPrivateLobbyButton === null)
throw new Error("Missing join-private-lobby-button");
joinPrivateLobbyButton.addEventListener("click", () => {
if (this.usernameInput?.isValid()) {
this.joinModal.open();
}
});
if (this.userSettings.darkMode()) {
document.documentElement.classList.add("dark");
@@ -190,10 +197,10 @@ class Client {
gameID: lobby.gameID,
serverConfig: config,
flag:
this.flagInput.getCurrentFlag() == "xx"
this.flagInput === null || this.flagInput.getCurrentFlag() == "xx"
? ""
: this.flagInput.getCurrentFlag(),
playerName: this.usernameInput.getCurrentUsername(),
playerName: this.usernameInput?.getCurrentUsername() ?? "",
persistentID: getPersistentIDFromCookie(),
clientID: lobby.clientID,
gameStartInfo: lobby.gameStartInfo ?? lobby.gameRecord?.gameStartInfo,
+5 -8
View File
@@ -14,7 +14,7 @@ export class PublicLobby extends LitElement {
@state() public isLobbyHighlighted: boolean = false;
@state() private isButtonDebounced: boolean = false;
private lobbiesInterval: number | null = null;
private currLobby: GameInfo = null;
private currLobby: GameInfo | null = null;
private debounceDelay: number = 750;
private lobbyIDToStart = new Map<GameID, number>();
@@ -46,7 +46,8 @@ export class PublicLobby extends LitElement {
// Store the start time on first fetch because endpoint is cached, causing
// the time to appear irregular.
if (!this.lobbyIDToStart.has(l.gameID)) {
this.lobbyIDToStart.set(l.gameID, l.msUntilStart + Date.now());
const msUntilStart = l.msUntilStart ?? 0;
this.lobbyIDToStart.set(l.gameID, msUntilStart + Date.now());
}
});
} catch (error) {
@@ -82,17 +83,13 @@ export class PublicLobby extends LitElement {
if (!lobby?.gameConfig) {
return;
}
const timeRemaining = Math.max(
0,
Math.floor((this.lobbyIDToStart.get(lobby.gameID) - Date.now()) / 1000),
);
const start = this.lobbyIDToStart.get(lobby.gameID) ?? 0;
const timeRemaining = Math.max(0, Math.floor((start - Date.now()) / 1000));
// Format time to show minutes and seconds
const minutes = Math.floor(timeRemaining / 60);
const seconds = timeRemaining % 60;
const timeDisplay = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
const playersRemainingBeforeMax =
lobby.gameConfig.maxPlayers - lobby.numClients;
return html`
<button
+15 -4
View File
@@ -60,14 +60,14 @@ export class SendSpawnIntentEvent implements GameEvent {
export class SendAttackIntentEvent implements GameEvent {
constructor(
public readonly targetID: PlayerID,
public readonly targetID: PlayerID | null,
public readonly troops: number,
) {}
}
export class SendBoatAttackIntentEvent implements GameEvent {
constructor(
public readonly targetID: PlayerID,
public readonly targetID: PlayerID | null,
public readonly cell: Cell,
public readonly troops: number,
) {}
@@ -148,7 +148,7 @@ export class MoveWarshipIntentEvent implements GameEvent {
}
export class Transport {
private socket: WebSocket;
private socket: WebSocket | null;
private localServer: LocalServer;
@@ -279,7 +279,12 @@ export class Transport {
console.log("Connected to game server!");
while (this.buffer.length > 0) {
console.log("sending dropped message");
this.sendMsg(this.buffer.pop());
const msg = this.buffer.pop();
if (typeof msg === "undefined") {
console.warn("msg is undefined");
continue;
}
this.sendMsg(msg);
}
onconnect();
};
@@ -295,6 +300,7 @@ export class Transport {
};
this.socket.onerror = (err) => {
console.error("Socket encountered error: ", err, "Closing socket");
if (this.socket === null) return;
this.socket.close();
};
this.socket.onclose = (event: CloseEvent) => {
@@ -349,6 +355,7 @@ export class Transport {
return;
}
this.stopPing();
if (this.socket === null) return;
if (this.socket.readyState === WebSocket.OPEN) {
console.log("on stop: leaving game");
this.socket.close();
@@ -495,6 +502,7 @@ export class Transport {
}
private onSendWinnerEvent(event: SendWinnerEvent) {
if (this.socket === null) return;
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
const msg = ClientSendWinnerSchema.parse({
type: "winner",
@@ -516,6 +524,7 @@ export class Transport {
}
private onSendHashEvent(event: SendHashEvent) {
if (this.socket === null) return;
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
const msg = ClientMessageSchema.parse({
type: "hash",
@@ -553,6 +562,7 @@ export class Transport {
}
private sendIntent(intent: Intent) {
if (this.socket === null) return;
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
const msg = ClientIntentMessageSchema.parse({
type: "intent",
@@ -575,6 +585,7 @@ export class Transport {
if (this.isLocal) {
this.localServer.onMessage(msg);
} else {
if (this.socket === null) return;
if (
this.socket.readyState == WebSocket.CLOSED ||
this.socket.readyState == WebSocket.CLOSED
+1 -1
View File
@@ -64,7 +64,7 @@ export class UsernameInput extends LitElement {
this.storeUsername(this.username);
this.validationError = "";
} else {
this.validationError = result.error;
this.validationError = result.error ?? "";
}
}
+3 -1
View File
@@ -176,7 +176,9 @@ export class GameRenderer {
public uiState: UIState,
private layers: Layer[],
) {
this.context = canvas.getContext("2d");
const context = canvas.getContext("2d");
if (context === null) throw new Error("2d context not supported");
this.context = context;
}
initialize() {
+4 -2
View File
@@ -9,8 +9,8 @@ export class TransformHandler {
private offsetX: number = -350;
private offsetY: number = -200;
private target: Cell;
private intervalID = null;
private target: Cell | null;
private intervalID: NodeJS.Timeout | null = null;
private changed = false;
constructor(
@@ -166,6 +166,8 @@ export class TransformHandler {
const { screenX, screenY } = this.screenCenter();
const screenMapCenter = new Cell(screenX, screenY);
if (this.target === null) throw new Error("null target");
if (
this.game.manhattanDist(
this.game.ref(screenX, screenY),
+1 -1
View File
@@ -398,7 +398,7 @@ export class BuildMenu extends LitElement implements Layer {
private refresh() {
this.game
.myPlayer()
.actions(this.clickedTile)
?.actions(this.clickedTile)
.then((actions) => {
this.playerActions = actions;
this.requestUpdate();
+1 -1
View File
@@ -253,7 +253,7 @@ export class ControlPanel extends LitElement implements Layer {
<label class="block text-white mb-1" translate="no"
>Attack Ratio: ${(this.attackRatio * 100).toFixed(0)}%
(${renderTroops(
this.game?.myPlayer()?.troops() * this.attackRatio,
(this.game?.myPlayer()?.troops() ?? 0) * this.attackRatio,
)})</label
>
<div class="relative h-8">
+11 -5
View File
@@ -100,8 +100,10 @@ export class EventsDisplay extends LitElement implements Layer {
tick() {
this.active = true;
const updates = this.game.updatesSinceLastTick();
for (const [ut, fn] of this.updateMap) {
updates[ut]?.forEach((u) => fn(u));
if (updates) {
for (const [ut, fn] of this.updateMap) {
updates[ut]?.forEach(fn);
}
}
let remainingEvents = this.events.filter((event) => {
@@ -301,6 +303,7 @@ export class EventsDisplay extends LitElement implements Layer {
: update.player2ID === myPlayer.smallID()
? update.player1ID
: null;
if (otherID === null) return;
const other = this.game.playerBySmallID(otherID) as PlayerView;
if (!other || !myPlayer.isAlive() || !other.isAlive()) return;
@@ -414,8 +417,10 @@ export class EventsDisplay extends LitElement implements Layer {
<button
translate="no"
class="ml-2"
@click=${() =>
this.emitGoToPlayerEvent(attack.attackerID)}
@click=${() => {
attack.attackerID &&
this.emitGoToPlayerEvent(attack.attackerID);
}}
>
${renderTroops(attack.troops)}
${(
@@ -597,7 +602,8 @@ export class EventsDisplay extends LitElement implements Layer {
${event.focusID
? html`<button
@click=${() => {
this.emitGoToPlayerEvent(event.focusID);
event.focusID &&
this.emitGoToPlayerEvent(event.focusID);
}}
>
${this.getEventDescription(event)}
+7 -6
View File
@@ -21,7 +21,7 @@ class RenderInfo {
constructor(
public player: PlayerView,
public lastRenderCalc: number,
public location: Cell,
public location: Cell | null,
public fontSize: number,
public fontColor: string,
public element: HTMLElement,
@@ -372,7 +372,7 @@ export class NameLayer implements Layer {
emoji.recipientID == myPlayer?.smallID(),
);
if (this.game.config().userSettings().emojis() && emojis.length > 0) {
if (this.game.config().userSettings()?.emojis() && emojis.length > 0) {
if (!existingEmoji) {
const emojiDiv = document.createElement("div");
emojiDiv.setAttribute("data-icon", "emoji");
@@ -418,8 +418,9 @@ export class NameLayer implements Layer {
});
const isMyPlayerTarget = nukesSentByOtherPlayer.find((unit) => {
const detonationDst = unit.detonationDst();
if (typeof detonationDst === "undefined") return false;
const targetId = this.game.owner(detonationDst).id();
return myPlayer && targetId == this.myPlayer.id();
return myPlayer && targetId == myPlayer.id();
});
const existingNuke = iconsDiv.querySelector(
'[data-icon="nuke"]',
@@ -484,9 +485,9 @@ export class NameLayer implements Layer {
if (this.myPlayer != null) {
return this.myPlayer;
}
this.myPlayer = this.game
.playerViews()
.find((p) => p.clientID() == this.clientID);
this.myPlayer =
this.game.playerViews().find((p) => p.clientID() == this.clientID) ??
null;
return this.myPlayer;
}
}
+4 -3
View File
@@ -124,9 +124,10 @@ export class OptionsMenu extends LitElement implements Layer {
}
tick() {
this.hasWinner =
this.hasWinner ||
this.game.updatesSinceLastTick()[GameUpdateType.Win].length > 0;
const updates = this.game.updatesSinceLastTick();
if (updates) {
this.hasWinner = this.hasWinner || updates[GameUpdateType.Win].length > 0;
}
if (this.game.inSpawnPhase()) {
this.timer = 0;
} else if (!this.hasWinner && this.game.ticks() % 10 == 0) {
@@ -1,4 +1,4 @@
import { LitElement, html } from "lit";
import { LitElement, TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import {
@@ -161,7 +161,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
private renderPlayerInfo(player: PlayerView) {
const myPlayer = this.myPlayer();
const isFriendly = myPlayer?.isFriendly(player);
let relationHtml = null;
let relationHtml: TemplateResult | null = null;
const attackingTroops = player
.outgoingAttacks()
.map((a) => a.troops)
+16 -14
View File
@@ -35,8 +35,8 @@ export class PlayerPanel extends LitElement implements Layer {
public eventBus: EventBus;
public emojiTable: EmojiTable;
private actions: PlayerActions = null;
private tile: TileRef = null;
private actions: PlayerActions | null = null;
private tile: TileRef | null = null;
@state()
private isVisible: boolean = false;
@@ -155,7 +155,11 @@ export class PlayerPanel extends LitElement implements Layer {
return 0;
}
let sum = 0;
const nukes = stats.sentNukes[this.g.myPlayer().id()];
const player = this.g.myPlayer();
if (player === null) {
return 0;
}
const nukes = stats.sentNukes[player.id()];
if (!nukes) {
return 0;
}
@@ -172,26 +176,24 @@ export class PlayerPanel extends LitElement implements Layer {
return html``;
}
const myPlayer = this.g.myPlayer();
if (myPlayer == null) {
return;
}
if (myPlayer == null) return;
if (this.tile === null) return;
let other = this.g.owner(this.tile);
if (!other.isPlayer()) {
throw new Error("Tile is not owned by a player");
}
other = other as PlayerView;
const canDonate = this.actions.interaction?.canDonate;
const canDonate = this.actions?.interaction?.canDonate;
const canSendAllianceRequest =
this.actions.interaction?.canSendAllianceRequest;
this.actions?.interaction?.canSendAllianceRequest;
const canSendEmoji =
other == myPlayer
? this.actions.canSendEmojiAllPlayers
: this.actions.interaction?.canSendEmoji;
const canBreakAlliance = this.actions.interaction?.canBreakAlliance;
const canTarget = this.actions.interaction?.canTarget;
const canEmbargo = this.actions.interaction?.canEmbargo;
? this.actions?.canSendEmojiAllPlayers
: this.actions?.interaction?.canSendEmoji;
const canBreakAlliance = this.actions?.interaction?.canBreakAlliance;
const canTarget = this.actions?.interaction?.canTarget;
const canEmbargo = this.actions?.interaction?.canEmbargo;
return html`
<div
+13 -1
View File
@@ -46,7 +46,16 @@ export class RadialMenu implements Layer {
private menuElement: d3.Selection<HTMLDivElement, unknown, null, undefined>;
private isVisible: boolean = false;
private readonly menuItems = new Map([
private readonly menuItems: Map<
Slot,
{
name: string;
disabled: boolean;
action: () => void;
color?: string | null;
icon?: string | null;
}
> = new Map([
[
Slot.Boat,
{
@@ -363,6 +372,7 @@ export class RadialMenu implements Layer {
}
if (actions.canBoat) {
this.activateMenuElement(Slot.Boat, "#3f6ab1", boatIcon, () => {
if (this.clickedCell === null) return;
this.eventBus.emit(
new SendBoatAttackIntentEvent(
this.g.owner(tile).id(),
@@ -412,6 +422,7 @@ export class RadialMenu implements Layer {
return;
}
consolex.log("Center button clicked");
if (this.clickedCell === null) return;
const clicked = this.g.ref(this.clickedCell.x, this.clickedCell.y);
if (this.g.inSpawnPhase()) {
this.eventBus.emit(new SendSpawnIntentEvent(this.clickedCell));
@@ -436,6 +447,7 @@ export class RadialMenu implements Layer {
action: () => void,
) {
const menuItem = this.menuItems.get(slot);
if (typeof menuItem === "undefined") return;
menuItem.action = action;
menuItem.disabled = false;
menuItem.color = color;
+13 -8
View File
@@ -43,7 +43,7 @@ export class StructureLayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private unitIcons: Map<string, ImageData> = new Map();
private theme: Theme = null;
private theme: Theme;
// Configuration for supported unit types only
private readonly unitConfigs: Partial<Record<UnitType, UnitRenderConfig>> = {
@@ -106,6 +106,7 @@ export class StructureLayer implements Layer {
// Create temporary canvas for icon processing
const tempCanvas = document.createElement("canvas");
const tempContext = tempCanvas.getContext("2d");
if (tempContext === null) throw new Error("2d context not supported");
tempCanvas.width = image.width;
tempCanvas.height = image.height;
@@ -135,11 +136,13 @@ export class StructureLayer implements Layer {
}
tick() {
this.game
.updatesSinceLastTick()
[
GameUpdateType.Unit
].forEach((u) => this.handleUnitRendering(this.game.unit(u.id)));
const updates = this.game.updatesSinceLastTick();
const unitUpdates = updates !== null ? updates[GameUpdateType.Unit] : [];
for (const u of unitUpdates) {
const unit = this.game.unit(u.id);
if (typeof unit === "undefined") continue;
this.handleUnitRendering(unit);
}
}
init() {
@@ -149,7 +152,9 @@ export class StructureLayer implements Layer {
redraw() {
console.log("structure layer redrawing");
this.canvas = document.createElement("canvas");
this.context = this.canvas.getContext("2d", { alpha: true });
const context = this.canvas.getContext("2d", { alpha: true });
if (context === null) throw new Error("2d context not supported");
this.context = context;
this.canvas.width = this.game.width();
this.canvas.height = this.game.height();
this.game.units().forEach((u) => this.handleUnitRendering(u));
@@ -220,7 +225,7 @@ export class StructureLayer implements Layer {
if (!this.isUnitTypeSupported(unitType)) return;
const config = this.unitConfigs[unitType];
let icon: ImageData;
let icon: ImageData | undefined;
if (unitType == UnitType.SAMLauncher && unit.isCooldown()) {
icon = this.unitIcons.get("reloadingSam");
+3 -1
View File
@@ -29,7 +29,9 @@ export class TerrainLayer implements Layer {
redraw(): void {
this.canvas = document.createElement("canvas");
this.context = this.canvas.getContext("2d");
const context = this.canvas.getContext("2d");
if (context === null) throw new Error("2d context not supported");
this.context = context;
this.imageData = this.context.getImageData(
0,
+11 -6
View File
@@ -8,7 +8,7 @@ import {
manhattanDistFN,
TileRef,
} from "../../../core/game/GameMap";
import { GameUpdateType, UnitUpdate } from "../../../core/game/GameUpdates";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { PseudoRandom } from "../../../core/PseudoRandom";
import { AlternateViewEvent, DragEvent } from "../../InputHandler";
@@ -26,7 +26,7 @@ export class TerritoryLayer implements Layer {
return a.lastUpdate - b.lastUpdate;
});
private random = new PseudoRandom(123);
private theme: Theme = null;
private theme: Theme;
// Used for spawn highlighting
private highlightCanvas: HTMLCanvasElement;
@@ -62,8 +62,9 @@ export class TerritoryLayer implements Layer {
tick() {
this.game.recentlyUpdatedTiles().forEach((t) => this.enqueueTile(t));
this.game.updatesSinceLastTick()[GameUpdateType.Unit].forEach((u) => {
const update = u as UnitUpdate;
const updates = this.game.updatesSinceLastTick();
const unitUpdates = updates !== null ? updates[GameUpdateType.Unit] : [];
unitUpdates.forEach((update) => {
if (update.unitType == UnitType.DefensePost && update.isActive) {
const tile = update.pos;
this.game
@@ -148,7 +149,9 @@ export class TerritoryLayer implements Layer {
redraw() {
console.log("redrew territory layer");
this.canvas = document.createElement("canvas");
this.context = this.canvas.getContext("2d");
const context = this.canvas.getContext("2d");
if (context === null) throw new Error("2d context not supported");
this.context = context;
this.imageData = this.context.getImageData(
0,
@@ -163,9 +166,11 @@ export class TerritoryLayer implements Layer {
// Add a second canvas for highlights
this.highlightCanvas = document.createElement("canvas");
this.highlightContext = this.highlightCanvas.getContext("2d", {
const highlightContext = this.highlightCanvas.getContext("2d", {
alpha: true,
});
if (highlightContext === null) throw new Error("2d context not supported");
this.highlightContext = highlightContext;
this.highlightCanvas.width = this.game.width();
this.highlightCanvas.height = this.game.height();
+12 -9
View File
@@ -22,18 +22,21 @@ export class TopBar extends LitElement implements Layer {
}
tick() {
if (this.game?.myPlayer() !== null) {
const popIncreaseRate =
this.game.myPlayer().population() - this._population;
if (this.game.ticks() % 5 == 0) {
this._popRateIsIncreasing =
popIncreaseRate >= this._lastPopulationIncreaseRate;
this._lastPopulationIncreaseRate = popIncreaseRate;
}
}
this.updatePopulationIncrease();
this.requestUpdate();
}
private updatePopulationIncrease() {
const player = this.game?.myPlayer();
if (player === null) return;
const popIncreaseRate = player.population() - this._population;
if (this.game.ticks() % 5 == 0) {
this._popRateIsIncreasing =
popIncreaseRate >= this._lastPopulationIncreaseRate;
this._lastPopulationIncreaseRate = popIncreaseRate;
}
}
render() {
if (!this.isVisible) {
return html``;
+5 -2
View File
@@ -14,9 +14,9 @@ import { Layer } from "./Layer";
*/
export class UILayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private context: CanvasRenderingContext2D | null;
private theme: Theme = null;
private theme: Theme | null = null;
private selectionAnimTime = 0;
// Keep track of currently selected unit
@@ -136,6 +136,7 @@ export class UILayer implements Layer {
baseOpacity + Math.sin(this.selectionAnimTime * 0.1) * pulseAmount;
// Get the unit's owner color for the box
if (this.theme === null) throw new Error("missing theme");
const ownerColor = this.theme.territoryColor(unit.owner());
// Create a brighter version of the owner color for the selection
@@ -196,12 +197,14 @@ export class UILayer implements Layer {
}
paintCell(x: number, y: number, color: Colord, alpha: number) {
if (this.context === null) throw new Error("null context");
this.clearCell(x, y);
this.context.fillStyle = color.alpha(alpha / 255).toRgbString();
this.context.fillRect(x, y, 1, 1);
}
clearCell(x: number, y: number) {
if (this.context === null) throw new Error("null context");
this.context.clearRect(x, y, 1, 1);
}
}
+27 -13
View File
@@ -33,7 +33,7 @@ export class UnitLayer implements Layer {
private boatToTrail = new Map<UnitView, TileRef[]>();
private theme: Theme = null;
private theme: Theme;
private alternateView = false;
@@ -67,9 +67,13 @@ export class UnitLayer implements Layer {
if (this.myPlayer == null) {
this.myPlayer = this.game.playerByClientID(this.clientID);
}
this.game.updatesSinceLastTick()?.[GameUpdateType.Unit]?.forEach((unit) => {
this.onUnitEvent(this.game.unit(unit.id));
});
const updates = this.game.updatesSinceLastTick();
const unitUpdates = updates?.[GameUpdateType.Unit] ?? [];
for (const u of unitUpdates) {
const unit = this.game.unit(u.id);
if (typeof unit === "undefined") continue;
this.onUnitEvent(unit);
}
}
init() {
@@ -185,20 +189,29 @@ export class UnitLayer implements Layer {
redraw() {
this.canvas = document.createElement("canvas");
this.context = this.canvas.getContext("2d");
const context = this.canvas.getContext("2d");
if (context === null) throw new Error("2d context not supported");
this.context = context;
this.transportShipTrailCanvas = document.createElement("canvas");
this.transportShipTrailContext =
const transportShipTrailContext =
this.transportShipTrailCanvas.getContext("2d");
if (transportShipTrailContext === null) {
throw new Error("2d context not supported");
}
this.transportShipTrailContext = transportShipTrailContext;
this.canvas.width = this.game.width();
this.canvas.height = this.game.height();
this.transportShipTrailCanvas.width = this.game.width();
this.transportShipTrailCanvas.height = this.game.height();
this.game
?.updatesSinceLastTick()
?.[GameUpdateType.Unit]?.forEach((unit) => {
this.onUnitEvent(this.game.unit(unit.id));
});
const updates = this.game.updatesSinceLastTick();
const unitUpdates = updates?.[GameUpdateType.Unit] ?? [];
for (const u of unitUpdates) {
const unit = this.game.unit(u.id);
if (typeof unit === "undefined") continue;
this.onUnitEvent(unit);
}
}
private relationship(unit: UnitView): Relationship {
@@ -315,8 +328,8 @@ export class UnitLayer implements Layer {
// Clear current and previous positions
this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile()));
if (this.oldShellTile.has(unit)) {
const oldTile = this.oldShellTile.get(unit);
const oldTile = this.oldShellTile.get(unit);
if (typeof oldTile !== "undefined") {
this.clearCell(this.game.x(oldTile), this.game.y(oldTile));
}
@@ -495,6 +508,7 @@ export class UnitLayer implements Layer {
this.boatToTrail.set(unit, []);
}
const trail = this.boatToTrail.get(unit);
if (typeof trail === "undefined") return;
trail.push(unit.lastTile());
// Clear previous area
+9 -4
View File
@@ -223,7 +223,9 @@ export class WinModal extends LitElement implements Layer {
this.won = false;
this.show();
}
this.game.updatesSinceLastTick()[GameUpdateType.Win].forEach((wu) => {
const updates = this.game.updatesSinceLastTick();
const winUpdates = updates !== null ? updates[GameUpdateType.Win] : [];
winUpdates.forEach((wu) => {
if (wu.winnerType === "team") {
this.eventBus.emit(
new SendWinnerEvent(wu.winner as Team, wu.allPlayersStats, "team"),
@@ -240,9 +242,12 @@ export class WinModal extends LitElement implements Layer {
const winner = this.game.playerBySmallID(
wu.winner as number,
) as PlayerView;
this.eventBus.emit(
new SendWinnerEvent(winner.clientID(), wu.allPlayersStats, "player"),
);
const winnerClient = winner.clientID();
if (winnerClient !== null) {
this.eventBus.emit(
new SendWinnerEvent(winnerClient, wu.allPlayersStats, "player"),
);
}
if (winner == this.game.myPlayer()) {
this._title = "You Won!";
this.won = true;
+3 -1
View File
@@ -116,8 +116,10 @@ export class GameRunner {
errMsg: error.message,
stack: error.stack,
} as ErrorUpdate);
return;
} else {
console.error("Game tick error:", error);
}
return;
}
if (this.game.inSpawnPhase() && this.game.ticks() % 2 == 0) {
+1 -1
View File
@@ -194,7 +194,7 @@ export const SpawnIntentSchema = BaseIntentSchema.extend({
export const BoatAttackIntentSchema = BaseIntentSchema.extend({
type: z.literal("boat"),
targetID: ID.nullable(),
troops: z.number().nullable(),
troops: z.number(),
x: z.number(),
y: z.number(),
});
+7 -8
View File
@@ -108,7 +108,7 @@ function closestShoreTN(
gm: GameMap,
tile: TileRef,
searchDist: number,
): TileRef {
): TileRef | null {
const tn = Array.from(
gm.bfs(
tile,
@@ -260,12 +260,17 @@ export function createGameRecord(
const record: GameRecord = {
id: id,
gameStartInfo: gameStart,
players,
startTimestampMS: start,
endTimestampMS: end,
durationSeconds: Math.floor((end - start) / 1000),
date: new Date().toISOString().split("T")[0],
num_turns: 0,
turns: [],
allPlayersStats,
version: "v0.0.1",
winner,
winnerType,
};
for (const turn of turns) {
@@ -282,18 +287,12 @@ export function createGameRecord(
}
}
}
record.players = players;
record.durationSeconds = Math.floor(
(record.endTimestampMS - record.startTimestampMS) / 1000,
);
record.num_turns = turns.length;
record.winner = winner;
record.winnerType = winnerType;
return record;
}
export function decompressGameRecord(gameRecord: GameRecord) {
const turns = [];
const turns: Turn[] = [];
let lastTurnNum = -1;
for (const turn of gameRecord.turns) {
while (lastTurnNum < turn.turnNumber - 1) {
+1 -1
View File
@@ -64,7 +64,7 @@ export interface Config {
infiniteTroops(): boolean;
instantBuild(): boolean;
numSpawnPhaseTurns(): number;
userSettings(): UserSettings;
userSettings(): UserSettings | null;
startManpower(playerInfo: PlayerInfo): number;
populationIncreaseRate(player: Player | PlayerView): number;
+2 -2
View File
@@ -7,7 +7,7 @@ import { DevConfig, DevServerConfig } from "./DevConfig";
import { preprodConfig } from "./PreprodConfig";
import { prodConfig } from "./ProdConfig";
export let cachedSC: ServerConfig = null;
export let cachedSC: ServerConfig | null = null;
export async function getConfig(
gameConfig: GameConfig,
@@ -44,7 +44,7 @@ export async function getServerConfigFromClient(): Promise<ServerConfig> {
return cachedSC;
}
export function getServerConfigFromServer(): ServerConfig {
const gameEnv = process.env.GAME_ENV;
const gameEnv = process.env.GAME_ENV ?? "dev";
return getServerConfig(gameEnv);
}
export function getServerConfig(gameEnv: string) {
+9 -8
View File
@@ -28,26 +28,26 @@ export abstract class DefaultServerConfig implements ServerConfig {
if (this.env() == GameEnv.Dev) {
return "dev";
}
return process.env.REGION;
return process.env.REGION ?? "undefined";
}
gitCommit(): string {
return process.env.GIT_COMMIT;
return process.env.GIT_COMMIT ?? "undefined";
}
r2Endpoint(): string {
return process.env.R2_ENDPOINT;
return process.env.R2_ENDPOINT ?? "undefined";
}
r2AccessKey(): string {
return process.env.R2_ACCESS_KEY;
return process.env.R2_ACCESS_KEY ?? "undefined";
}
r2SecretKey(): string {
return process.env.R2_SECRET_KEY;
return process.env.R2_SECRET_KEY ?? "undefined";
}
abstract r2Bucket(): string;
adminHeader(): string {
return "x-admin-key";
}
adminToken(): string {
return process.env.ADMIN_TOKEN;
return process.env.ADMIN_TOKEN ?? "undefined";
}
abstract numWorkers(): number;
abstract env(): GameEnv;
@@ -128,7 +128,7 @@ export class DefaultConfig implements Config {
constructor(
private _serverConfig: ServerConfig,
private _gameConfig: GameConfig,
private _userSettings: UserSettings,
private _userSettings: UserSettings | null,
) {}
samHittingChance(): number {
@@ -394,7 +394,7 @@ export class DefaultConfig implements Config {
return this.bots();
}
theme(): Theme {
return this.userSettings().darkMode() ? pastelThemeDark : pastelTheme;
return this.userSettings()?.darkMode() ? pastelThemeDark : pastelTheme;
}
attackLogic(
@@ -647,6 +647,7 @@ export class DefaultConfig implements Config {
case UnitType.HydrogenBomb:
return { inner: 80, outer: 100 };
}
throw new Error(`Unknown nuke type: ${unitType}`);
}
defaultNukeSpeed(): number {
+1 -1
View File
@@ -36,7 +36,7 @@ export class DevServerConfig extends DefaultServerConfig {
}
export class DevConfig extends DefaultConfig {
constructor(sc: ServerConfig, gc: GameConfig, us: UserSettings) {
constructor(sc: ServerConfig, gc: GameConfig, us: UserSettings | null) {
super(sc, gc, us);
}
+10 -2
View File
@@ -39,7 +39,7 @@ export class AttackExecution implements Execution {
private border = new Set<TileRef>();
private attack: Attack = null;
private attack: Attack | null = null;
constructor(
private startTroops: number | null = null,
@@ -49,7 +49,7 @@ export class AttackExecution implements Execution {
private removeTroops: boolean = true,
) {}
public targetID(): PlayerID {
public targetID(): PlayerID | null {
return this._targetID;
}
@@ -175,6 +175,10 @@ export class AttackExecution implements Execution {
}
private retreat(malusPercent = 0) {
if (this.attack === null) {
throw new Error("Attack not initialized");
}
const deaths = this.attack.troops() * (malusPercent / 100);
if (deaths) {
this.mg.displayMessage(
@@ -189,6 +193,10 @@ export class AttackExecution implements Execution {
}
tick(ticks: number) {
if (this.attack === null) {
throw new Error("Attack not initialized");
}
if (this.attack.retreated()) {
if (this.attack.target().isPlayer()) {
this.retreat(malusForRetreat);
+1 -4
View File
@@ -33,6 +33,7 @@ export class DonateGoldExecution implements Execution {
}
tick(ticks: number): void {
if (this.gold === null) throw new Error("not initialized");
if (this.sender.canDonate(this.recipient)) {
this.sender.donateGold(this.recipient, this.gold);
this.recipient.updateRelation(this.sender, 50);
@@ -44,10 +45,6 @@ export class DonateGoldExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
+1 -4
View File
@@ -33,6 +33,7 @@ export class DonateTroopsExecution implements Execution {
}
tick(ticks: number): void {
if (this.troops === null) throw new Error("not initialized");
if (this.sender.canDonate(this.recipient)) {
this.sender.donateTroops(this.recipient, this.troops);
this.recipient.updateRelation(this.sender, 50);
@@ -44,10 +45,6 @@ export class DonateTroopsExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
-4
View File
@@ -29,10 +29,6 @@ export class EmbargoExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
-4
View File
@@ -55,10 +55,6 @@ export class EmojiExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
+2 -2
View File
@@ -23,7 +23,7 @@ import { TransportShipExecution } from "./TransportShipExecution";
export class Executor {
// private random = new PseudoRandom(999)
private random: PseudoRandom = null;
private random: PseudoRandom;
constructor(
private mg: Game,
@@ -113,7 +113,7 @@ export class Executor {
}
fakeHumanExecutions(): Execution[] {
const execs = [];
const execs: Execution[] = [];
for (const nation of this.mg.nations()) {
execs.push(
new FakeHumanExecution(
+57 -38
View File
@@ -34,7 +34,7 @@ export class FakeHumanExecution implements Execution {
private active = true;
private random: PseudoRandom;
private mg: Game;
private player: Player = null;
private player: Player | null = null;
private enemy: Player | null = null;
@@ -59,41 +59,45 @@ export class FakeHumanExecution implements Execution {
}
private updateRelationsFromEmbargos() {
const others = this.mg.players().filter((p) => p.id() != this.player.id());
const player = this.player;
if (player === null) return;
const others = this.mg.players().filter((p) => p.id() != player.id());
others.forEach((other: Player) => {
const embargoMalus = -20;
if (
other.hasEmbargoAgainst(this.player) &&
other.hasEmbargoAgainst(player) &&
!this.embargoMalusApplied.has(other.id())
) {
this.player.updateRelation(other, embargoMalus);
player.updateRelation(other, embargoMalus);
this.embargoMalusApplied.add(other.id());
} else if (
!other.hasEmbargoAgainst(this.player) &&
!other.hasEmbargoAgainst(player) &&
this.embargoMalusApplied.has(other.id())
) {
this.player.updateRelation(other, -embargoMalus);
player.updateRelation(other, -embargoMalus);
this.embargoMalusApplied.delete(other.id());
}
});
}
private handleEmbargoesToHostileNations() {
const others = this.mg.players().filter((p) => p.id() != this.player.id());
const player = this.player;
if (player === null) return;
const others = this.mg.players().filter((p) => p.id() != player.id());
others.forEach((other: Player) => {
/* When player is hostile starts embargo. Do not stop until neutral again */
if (
this.player.relation(other) <= Relation.Hostile &&
!this.player.hasEmbargoAgainst(other)
player.relation(other) <= Relation.Hostile &&
!player.hasEmbargoAgainst(other)
) {
this.player.addEmbargo(other.id());
player.addEmbargo(other.id());
} else if (
this.player.relation(other) >= Relation.Neutral &&
this.player.hasEmbargoAgainst(other)
player.relation(other) >= Relation.Neutral &&
player.hasEmbargoAgainst(other)
) {
this.player.stopEmbargo(other.id());
player.stopEmbargo(other.id());
}
});
}
@@ -111,7 +115,8 @@ export class FakeHumanExecution implements Execution {
return;
}
if (this.player == null) {
this.player = this.mg.players().find((p) => p.id() == this.playerInfo.id);
this.player =
this.mg.players().find((p) => p.id() == this.playerInfo.id) ?? null;
if (this.player == null) {
return;
}
@@ -146,7 +151,8 @@ export class FakeHumanExecution implements Execution {
const enemyborder = Array.from(this.player.borderTiles())
.flatMap((t) => this.mg.neighbors(t))
.filter(
(t) => this.mg.isLand(t) && this.mg.ownerID(t) != this.player.smallID(),
(t) =>
this.mg.isLand(t) && this.mg.ownerID(t) != this.player?.smallID(),
);
if (enemyborder.length == 0) {
@@ -191,6 +197,7 @@ export class FakeHumanExecution implements Execution {
}
private shouldAttack(other: Player): boolean {
if (this.player === null) throw new Error("not initialized");
if (this.player.isOnSameTeam(other)) {
return false;
}
@@ -227,30 +234,33 @@ export class FakeHumanExecution implements Execution {
this.enemy = null;
}
const player = this.player;
if (player === null) return;
const target =
this.player
player
.allies()
.filter((ally) => this.player.relation(ally) == Relation.Friendly)
.filter((ally) => player.relation(ally) == Relation.Friendly)
.filter((ally) => ally.targets().length > 0)
.map((ally) => ({ ally: ally, t: ally.targets()[0] }))[0] ?? null;
if (
target != null &&
target.t != this.player &&
!this.player.isAlliedWith(target.t)
target.t != player &&
!player.isAlliedWith(target.t)
) {
this.player.updateRelation(target.ally, -20);
player.updateRelation(target.ally, -20);
this.enemy = target.t;
this.lastEnemyUpdateTick = this.mg.ticks();
if (target.ally.type() == PlayerType.Human) {
this.mg.addExecution(
new EmojiExecution(this.player.id(), target.ally.id(), "👍"),
new EmojiExecution(player.id(), target.ally.id(), "👍"),
);
}
}
if (this.enemy == null) {
const mostHated = this.player.allRelationsSorted()[0] ?? null;
const mostHated = player.allRelationsSorted()[0] ?? null;
if (mostHated != null && mostHated.relation == Relation.Hostile) {
this.enemy = mostHated.player;
this.lastEnemyUpdateTick = this.mg.ticks();
@@ -258,13 +268,13 @@ export class FakeHumanExecution implements Execution {
}
if (this.enemy) {
if (this.player.isFriendly(this.enemy)) {
if (player.isFriendly(this.enemy)) {
this.enemy = null;
return;
}
this.maybeSendEmoji();
this.maybeSendNuke(this.enemy);
if (this.player.sharesBorderWith(this.enemy)) {
if (player.sharesBorderWith(this.enemy)) {
this.sendAttack(this.enemy);
} else {
this.maybeSendBoatAttack(this.enemy);
@@ -274,6 +284,8 @@ export class FakeHumanExecution implements Execution {
}
private maybeSendEmoji() {
if (this.player === null) throw new Error("not initialized");
if (this.enemy === null) return;
if (this.enemy.type() != PlayerType.Human) return;
const lastSent = this.lastEmojiSent.get(this.enemy) ?? -300;
if (this.mg.ticks() - lastSent <= 300) return;
@@ -288,6 +300,7 @@ export class FakeHumanExecution implements Execution {
}
private maybeSendNuke(other: Player) {
if (this.player === null) throw new Error("not initialized");
if (
this.player.units(UnitType.MissileSilo).length == 0 ||
this.player.gold() <
@@ -317,6 +330,7 @@ export class FakeHumanExecution implements Execution {
}
private maybeSendBoatAttack(other: Player) {
if (this.player === null) throw new Error("not initialized");
if (this.player.isOnSameTeam(other)) return;
const closest = closestTwoTiles(
this.mg,
@@ -339,15 +353,17 @@ export class FakeHumanExecution implements Execution {
}
private handleUnits() {
const ports = this.player.units(UnitType.Port);
if (ports.length == 0 && this.player.gold() > this.cost(UnitType.Port)) {
const oceanTiles = Array.from(this.player.borderTiles()).filter((t) =>
const player = this.player;
if (player === null) return;
const ports = player.units(UnitType.Port);
if (ports.length == 0 && player.gold() > this.cost(UnitType.Port)) {
const oceanTiles = Array.from(player.borderTiles()).filter((t) =>
this.mg.isOceanShore(t),
);
if (oceanTiles.length > 0) {
const buildTile = this.random.randElement(oceanTiles);
this.mg.addExecution(
new ConstructionExecution(this.player.id(), buildTile, UnitType.Port),
new ConstructionExecution(player.id(), buildTile, UnitType.Port),
);
}
return;
@@ -355,7 +371,7 @@ export class FakeHumanExecution implements Execution {
this.maybeSpawnStructure(
UnitType.City,
2,
(t) => new ConstructionExecution(this.player.id(), t, UnitType.City),
(t) => new ConstructionExecution(player.id(), t, UnitType.City),
);
if (this.maybeSpawnWarship()) {
return;
@@ -364,8 +380,7 @@ export class FakeHumanExecution implements Execution {
this.maybeSpawnStructure(
UnitType.MissileSilo,
1,
(t) =>
new ConstructionExecution(this.player.id(), t, UnitType.MissileSilo),
(t) => new ConstructionExecution(player.id(), t, UnitType.MissileSilo),
);
}
}
@@ -375,6 +390,7 @@ export class FakeHumanExecution implements Execution {
maxNum: number,
build: (tile: TileRef) => Execution,
) {
if (this.player === null) throw new Error("not initialized");
const units = this.player.units(type);
if (units.length >= maxNum) {
return;
@@ -396,6 +412,7 @@ export class FakeHumanExecution implements Execution {
}
private maybeSpawnWarship(): boolean {
if (this.player === null) throw new Error("not initialized");
if (!this.random.chance(50)) {
return false;
}
@@ -470,10 +487,12 @@ export class FakeHumanExecution implements Execution {
}
private cost(type: UnitType): number {
if (this.player === null) throw new Error("not initialized");
return this.mg.unitInfo(type).cost(this.player);
}
handleAllianceRequests() {
if (this.player === null) throw new Error("not initialized");
for (const req of this.player.incomingAllianceRequests()) {
if (req.requestor().isTraitor()) {
this.replyToAllianceRequest(req, false);
@@ -494,6 +513,7 @@ export class FakeHumanExecution implements Execution {
}
private replyToAllianceRequest(req: AllianceRequest, accept: boolean): void {
if (this.player === null) throw new Error("not initialized");
this.mg.addExecution(
new AllianceRequestReplyExecution(
req.requestor().id(),
@@ -504,6 +524,7 @@ export class FakeHumanExecution implements Execution {
}
sendBoatRandomly() {
if (this.player === null) throw new Error("not initialized");
const oceanShore = Array.from(this.player.borderTiles()).filter((t) =>
this.mg.isOceanShore(t),
);
@@ -532,9 +553,9 @@ export class FakeHumanExecution implements Execution {
randomLand(): TileRef | null {
const delta = 25;
let tries = 0;
while (tries < 50) {
tries++;
const cell = this.playerInfo.nation.cell;
const cell = this.playerInfo.nation?.cell;
if (typeof cell === "undefined") return null;
while (tries++ < 50) {
const x = this.random.nextInt(cell.x - delta, cell.x + delta);
const y = this.random.nextInt(cell.y - delta, cell.y + delta);
if (!this.mg.isValidCoord(x, y)) {
@@ -555,6 +576,7 @@ export class FakeHumanExecution implements Execution {
}
sendAttack(toAttack: Player | TerraNullius) {
if (this.player === null) throw new Error("not initialized");
if (toAttack.isPlayer() && this.player.isOnSameTeam(toAttack)) return;
this.mg.addExecution(
new AttackExecution(
@@ -566,6 +588,7 @@ export class FakeHumanExecution implements Execution {
}
private randOceanShoreTile(tile: TileRef, dist: number): TileRef | null {
if (this.player === null) throw new Error("not initialized");
const x = this.mg.x(tile);
const y = this.mg.y(tile);
for (let i = 0; i < 500; i++) {
@@ -589,10 +612,6 @@ export class FakeHumanExecution implements Execution {
return null;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
+1 -4
View File
@@ -1,4 +1,4 @@
import { Execution, Game, Player } from "../game/Game";
import { Execution, Game } from "../game/Game";
export class NoOpExecution implements Execution {
isActive(): boolean {
@@ -9,7 +9,4 @@ export class NoOpExecution implements Execution {
}
init(mg: Game, ticks: number): void {}
tick(ticks: number): void {}
owner(): Player {
return null;
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ export class NukeExecution implements Execution {
private type: NukeType,
private senderID: PlayerID,
private dst: TileRef,
private src?: TileRef,
private src?: TileRef | null,
private speed: number = -1,
private waitTicks = 0,
) {}
+2
View File
@@ -115,6 +115,7 @@ export class PlayerExecution implements Execution {
clusters.sort((a, b) => b.size - a.size);
const main = clusters.shift();
if (typeof main === "undefined") throw new Error("No clusters");
this.player.largestClusterBoundingBox = calculateBoundingBox(this.mg, main);
const surroundedBy = this.surroundedBySamePlayer(main);
if (surroundedBy && !this.player.isFriendly(surroundedBy)) {
@@ -276,6 +277,7 @@ export class PlayerExecution implements Execution {
seen.add(tile);
while (queue.length > 0) {
const curr = queue.shift();
if (typeof curr === "undefined") throw new Error("curr is undefined");
cluster.add(curr);
const neighbors = (this.mg as GameImpl).neighborsWithDiag(curr);
+1 -1
View File
@@ -18,7 +18,7 @@ export class SAMLauncherExecution implements Execution {
private sam: Unit;
private active: boolean = true;
private target: Unit = null;
private target: Unit | null = null;
private searchRangeRadius = 75;
@@ -31,10 +31,6 @@ export class SetTargetTroopRatioExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
+1 -1
View File
@@ -55,7 +55,7 @@ export class ShellExecution implements Execution {
switch (result.type) {
case PathFindResultType.Completed:
this.active = false;
this.target.modifyHealth(-this.shell.info().damage);
this.target.modifyHealth(-(this.shell.info().damage ?? 0));
this.shell.delete(false);
return;
case PathFindResultType.NextTile:
+1 -4
View File
@@ -25,7 +25,7 @@ export class SpawnExecution implements Execution {
return;
}
let player: Player = null;
let player: Player | null = null;
if (this.mg.hasPlayer(this.playerInfo.id)) {
player = this.mg.player(this.playerInfo.id);
} else {
@@ -46,9 +46,6 @@ export class SpawnExecution implements Execution {
player.setHasSpawned(true);
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
@@ -37,10 +37,6 @@ export class TargetPlayerExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
+5 -1
View File
@@ -40,7 +40,7 @@ export class TransportShipExecution implements Execution {
private attackerID: PlayerID,
private targetID: PlayerID | null,
private ref: TileRef,
private troops: number | null,
private troops: number,
) {}
activeDuringSpawnPhase(): boolean {
@@ -129,6 +129,10 @@ export class TransportShipExecution implements Execution {
}
tick(ticks: number) {
if (this.dst == null) {
this.active = false;
return;
}
if (!this.active) {
return;
}
+1 -1
View File
@@ -10,7 +10,7 @@ export function closestTwoTiles(
gm: GameMap,
x: Iterable<TileRef>,
y: Iterable<TileRef>,
): { x: TileRef; y: TileRef } {
): { x: TileRef; y: TileRef } | null {
const xSorted = Array.from(x).sort((a, b) => gm.x(a) - gm.x(b));
const ySorted = Array.from(y).sort((a, b) => gm.x(a) - gm.x(b));
+20 -10
View File
@@ -18,10 +18,10 @@ export class WarshipExecution implements Execution {
private _owner: Player;
private active = true;
private warship: Unit = null;
private mg: Game = null;
private warship: Unit | null = null;
private mg: Game | null = null;
private target: Unit = null;
private target: Unit | null = null;
private pathfinder: PathFinder;
private patrolTile: TileRef;
@@ -53,7 +53,8 @@ export class WarshipExecution implements Execution {
}
// Only for warships with "moveTarget" set
goToMoveTarget(target: TileRef): boolean {
goToMoveTarget(target: TileRef) {
if (this.warship === null) throw new Error("Warship not initialized");
// Patrol unless we are hunting down a tradeship
const result = this.pathfinder.nextTile(this.warship.tile(), target);
switch (result.type) {
@@ -72,6 +73,9 @@ export class WarshipExecution implements Execution {
}
private shoot() {
if (this.mg === null) throw new Error("Game not initialized");
if (this.warship === null) throw new Error("Warship not initialized");
if (this.target === null) throw new Error("Target not initialized");
if (this.mg.ticks() - this.lastShellAttack > this.shellAttackRate) {
this.lastShellAttack = this.mg.ticks();
this.mg.addExecution(
@@ -92,6 +96,7 @@ export class WarshipExecution implements Execution {
}
private patrol() {
if (this.warship === null) throw new Error("Warship not initialized");
this.warship.setWarshipTarget(this.target);
if (this.target == null || this.target.type() != UnitType.TradeShip) {
// Patrol unless we are hunting down a tradeship
@@ -134,17 +139,20 @@ export class WarshipExecution implements Execution {
this.target = null;
}
const hasPort = this._owner.units(UnitType.Port).length > 0;
if (this.mg === null) throw new Error("Game not initialized");
const warship = this.warship;
if (warship === null) throw new Error("Warship not initialized");
const ships = this.mg
.nearbyUnits(
this.warship.tile(),
warship.tile(),
130, // Search range
[UnitType.TransportShip, UnitType.Warship, UnitType.TradeShip],
)
.filter(
({ unit }) =>
unit.owner() !== this.warship.owner() &&
unit !== this.warship &&
!unit.owner().isFriendly(this.warship.owner()) &&
unit.owner() !== warship.owner() &&
unit !== warship &&
!unit.owner().isFriendly(warship.owner()) &&
!this.alreadySentShell.has(unit) &&
(unit.type() !== UnitType.TradeShip || hasPort) &&
(unit.type() !== UnitType.TradeShip ||
@@ -184,8 +192,9 @@ export class WarshipExecution implements Execution {
return distA - distB;
})[0]?.unit ?? null;
if (this.warship.moveTarget()) {
this.goToMoveTarget(this.warship.moveTarget());
const moveTarget = this.warship.moveTarget();
if (moveTarget) {
this.goToMoveTarget(moveTarget);
// If we have a "move target" then we cannot target trade ships as it
// requires moving.
if (this.target && this.target.type() == UnitType.TradeShip) {
@@ -250,6 +259,7 @@ export class WarshipExecution implements Execution {
}
randomTile(): TileRef {
if (this.mg === null) throw new Error("Game not initialized");
while (true) {
const x =
this.mg.x(this.patrolCenterTile) +
+5 -5
View File
@@ -50,9 +50,12 @@ export class WinCheckExecution implements Execution {
checkWinnerTeam(): void {
const teamToTiles = new Map<Team, number>();
for (const player of this.mg.players()) {
const team = player.team();
// Sanity check, team should not be null here
if (team === null) continue;
teamToTiles.set(
player.team(),
(teamToTiles.get(player.team()) ?? 0) + player.numTilesOwned(),
team,
(teamToTiles.get(team) ?? 0) + player.numTilesOwned(),
);
}
const sorted = Array.from(teamToTiles.entries()).sort(
@@ -72,9 +75,6 @@ export class WinCheckExecution implements Execution {
this.active = false;
}
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
@@ -3,7 +3,6 @@ import { Execution, Game, Player, PlayerID } from "../../game/Game";
export class AllianceRequestExecution implements Execution {
private active = true;
private mg: Game = null;
private requestor: Player;
private recipient: Player;
@@ -28,7 +27,6 @@ export class AllianceRequestExecution implements Execution {
return;
}
this.mg = mg;
this.requestor = mg.player(this.requestorID);
this.recipient = mg.player(this.recipientID);
}
@@ -44,10 +42,6 @@ export class AllianceRequestExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
@@ -3,7 +3,6 @@ import { Execution, Game, Player, PlayerID } from "../../game/Game";
export class AllianceRequestReplyExecution implements Execution {
private active = true;
private mg: Game = null;
private requestor: Player;
private recipient: Player;
@@ -28,7 +27,6 @@ export class AllianceRequestReplyExecution implements Execution {
this.active = false;
return;
}
this.mg = mg;
this.requestor = mg.player(this.requestorID);
this.recipient = mg.player(this.recipientID);
}
@@ -55,10 +53,6 @@ export class AllianceRequestReplyExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
@@ -48,10 +48,6 @@ export class BreakAllianceExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
+10 -10
View File
@@ -212,7 +212,7 @@ export class PlayerInfo {
public readonly clan: string | null;
constructor(
public readonly flag: string,
public readonly flag: string | undefined,
public readonly name: string,
public readonly playerType: PlayerType,
// null if bot.
@@ -259,17 +259,17 @@ export interface Unit {
health(): number;
modifyHealth(delta: number): void;
setWarshipTarget(target: Unit): void; // warship only
warshipTarget(): Unit;
setWarshipTarget(target: Unit | null): void; // warship only
warshipTarget(): Unit | null;
setCooldown(triggerCooldown: boolean): void;
ticksLeftInCooldown(cooldownDuration: number): Tick;
isCooldown(): boolean;
setDstPort(dstPort: Unit): void;
dstPort(): Unit; // Only for trade ships
detonationDst(): TileRef; // Only for nukes
dstPort(): Unit | null; // Only for trade ships
detonationDst(): TileRef | null; // Only for nukes
setMoveTarget(cell: TileRef): void;
setMoveTarget(cell: TileRef | null): void;
moveTarget(): TileRef | null;
setTargetedBySAM(targeted: boolean): void;
@@ -289,7 +289,7 @@ export interface Unit {
export interface TerraNullius {
isPlayer(): false;
id(): PlayerID; // always zero, maybe make it TerraNulliusID?
id(): null;
clientID(): ClientID;
smallID(): number;
}
@@ -300,7 +300,7 @@ export interface Player {
info(): PlayerInfo;
name(): string;
displayName(): string;
clientID(): ClientID;
clientID(): ClientID | null;
id(): PlayerID;
type(): PlayerType;
isPlayer(): this is Player;
@@ -369,7 +369,7 @@ export interface Player {
allianceWith(other: Player): MutableAlliance | null;
canSendAllianceRequest(other: Player): boolean;
breakAlliance(alliance: Alliance): void;
createAllianceRequest(recipient: Player): AllianceRequest;
createAllianceRequest(recipient: Player): AllianceRequest | null;
// Targeting
canTarget(other: Player): boolean;
@@ -399,7 +399,7 @@ export interface Player {
createAttack(
target: Player | TerraNullius,
troops: number,
sourceTile: TileRef,
sourceTile: TileRef | null,
): Attack;
outgoingAttacks(): Attack[];
incomingAttacks(): Attack[];
+15 -11
View File
@@ -57,7 +57,7 @@ export class GameImpl implements Game {
private nations_: Nation[] = [];
_players: Map<PlayerID, PlayerImpl> = new Map<PlayerID, PlayerImpl>();
_playersBySmallID = [];
_playersBySmallID: Player[] = [];
private execs: Execution[] = [];
private _width: number;
@@ -170,10 +170,13 @@ export class GameImpl implements Game {
return this.nations_;
}
createAllianceRequest(requestor: Player, recipient: Player): AllianceRequest {
createAllianceRequest(
requestor: Player,
recipient: Player,
): AllianceRequest | null {
if (requestor.isAlliedWith(recipient)) {
consolex.log("cannot request alliance, already allied");
return;
return null;
}
if (
recipient
@@ -181,7 +184,7 @@ export class GameImpl implements Game {
.find((ar) => ar.requestor() == requestor) != null
) {
consolex.log(`duplicate alliance request from ${requestor.name()}`);
return;
return null;
}
const correspondingReq = requestor
.incomingAllianceRequests()
@@ -189,7 +192,7 @@ export class GameImpl implements Game {
if (correspondingReq != null) {
consolex.log(`got corresponding alliance requests, accepting`);
correspondingReq.accept();
return;
return null;
}
const ar = new AllianceRequestImpl(requestor, recipient, this._ticks, this);
this.allianceRequests.push(ar);
@@ -342,7 +345,7 @@ export class GameImpl implements Game {
return this.player(id);
}
addPlayer(playerInfo: PlayerInfo, team: Team = null): Player {
addPlayer(playerInfo: PlayerInfo, team: Team | null = null): Player {
const player = new PlayerImpl(
this,
this.nextPlayerID,
@@ -367,11 +370,12 @@ export class GameImpl implements Game {
return this.playerTeams[rand % this.playerTeams.length];
}
player(id: PlayerID | null): Player {
if (!this._players.has(id)) {
player(id: PlayerID): Player {
const player = this._players.get(id);
if (typeof player === "undefined") {
throw new Error(`Player with id ${id} not found`);
}
return this._players.get(id);
return player;
}
playerByClientID(id: ClientID): Player | null {
@@ -495,7 +499,7 @@ export class GameImpl implements Game {
}
public breakAlliance(breaker: Player, alliance: Alliance) {
let other: Player = null;
let other: Player;
if (alliance.requestor() == breaker) {
other = alliance.recipient();
} else {
@@ -572,7 +576,7 @@ export class GameImpl implements Game {
type: MessageType,
playerID: PlayerID | null,
): void {
let id = null;
let id: number | null = null;
if (playerID != null) {
id = this.player(playerID).smallID();
}
+1
View File
@@ -280,6 +280,7 @@ export class GameMapImpl implements GameMap {
q.push(tile);
while (q.length > 0) {
const curr = q.pop();
if (typeof curr === "undefined") continue;
for (const n of this.neighbors(curr)) {
if (!seen.has(n) && filter(this, n)) {
seen.add(n);
+3 -3
View File
@@ -14,7 +14,7 @@ import { TileRef, TileUpdate } from "./GameMap";
export interface GameUpdateViewData {
tick: number;
updates: GameUpdates;
updates: GameUpdates | null;
packedTileUpdates: BigUint64Array;
playerNameViewData: Record<number, NameViewData>;
}
@@ -87,8 +87,8 @@ export interface AttackUpdate {
export interface PlayerUpdate {
type: GameUpdateType.Player;
nameViewData?: NameViewData;
clientID: ClientID;
flag: string;
clientID: ClientID | null;
flag: string | undefined;
name: string;
displayName: string;
id: PlayerID;
+26 -20
View File
@@ -96,28 +96,29 @@ export class UnitView {
constructionType(): UnitType | undefined {
return this.data.constructionType;
}
dstPortId(): number {
dstPortId(): number | undefined {
if (this.type() != UnitType.TradeShip) {
throw Error("Must be a trade ship");
}
return this.data.dstPortId;
}
detonationDst(): TileRef {
detonationDst(): TileRef | undefined {
if (!nukeTypes.includes(this.type())) {
throw Error("Must be a nuke");
}
return this.data.detonationDst;
}
warshipTargetId(): number {
warshipTargetId(): number | undefined {
if (this.type() != UnitType.Warship) {
throw Error("Must be a warship");
}
return this.data.warshipTargetId;
}
ticksLeftInCooldown(): Tick {
ticksLeftInCooldown(): Tick | undefined {
return this.data.ticksLeftInCooldown;
}
isCooldown(): boolean {
if (typeof this.data.ticksLeftInCooldown === "undefined") return false;
return this.data.ticksLeftInCooldown > 0;
}
}
@@ -162,7 +163,7 @@ export class PlayerView {
smallID(): number {
return this.data.smallID;
}
flag(): string {
flag(): string | undefined {
return this.data.flag;
}
name(): string {
@@ -171,13 +172,13 @@ export class PlayerView {
displayName(): string {
return this.data.displayName;
}
clientID(): ClientID {
clientID(): ClientID | null {
return this.data.clientID;
}
id(): PlayerID {
return this.data.id;
}
team(): Team | null {
team(): Team | undefined {
return this.data.team;
}
type(): PlayerType {
@@ -303,7 +304,7 @@ export class GameView implements GameMap {
return this._map.isOnEdgeOfMap(ref);
}
public updatesSinceLastTick(): GameUpdates {
public updatesSinceLastTick(): GameUpdates | null {
return this.lastUpdate.updates;
}
@@ -318,11 +319,15 @@ export class GameView implements GameMap {
this.updatedTiles.push(this.updateTile(tu));
});
if (gu.updates === null) {
throw new Error("lastUpdate.updates not initialized");
}
gu.updates[GameUpdateType.Player].forEach((pu) => {
this.smallIDToID.set(pu.smallID, pu.id);
if (this._players.has(pu.id)) {
this._players.get(pu.id).data = pu;
this._players.get(pu.id).nameData = gu.playerNameViewData[pu.id];
const player = this._players.get(pu.id);
if (typeof player !== "undefined") {
player.data = pu;
player.nameData = gu.playerNameViewData[pu.id];
} else {
this._players.set(
pu.id,
@@ -335,9 +340,8 @@ export class GameView implements GameMap {
unit.lastPos = unit.lastPos.slice(-1);
}
gu.updates[GameUpdateType.Unit].forEach((update) => {
let unit: UnitView = null;
if (this._units.has(update.id)) {
unit = this._units.get(update.id);
let unit = this._units.get(update.id);
if (typeof unit !== "undefined") {
unit.update(update);
} else {
unit = new UnitView(this, update);
@@ -382,10 +386,11 @@ export class GameView implements GameMap {
}
player(id: PlayerID): PlayerView {
if (this._players.has(id)) {
return this._players.get(id);
const player = this._players.get(id);
if (typeof player === "undefined") {
throw Error(`player id ${id} not found`);
}
throw Error(`player id ${id} not found`);
return player;
}
players(): PlayerView[] {
@@ -396,10 +401,11 @@ export class GameView implements GameMap {
if (id == 0) {
return new TerraNulliusImpl();
}
if (!this.smallIDToID.has(id)) {
const playerId = this.smallIDToID.get(id);
if (typeof playerId === "undefined") {
throw new Error(`small id ${id} not found`);
}
return this.player(this.smallIDToID.get(id));
return this.player(playerId);
}
playerByClientID(id: ClientID): PlayerView | null {
@@ -439,7 +445,7 @@ export class GameView implements GameMap {
(u) => u.isActive() && types.includes(u.type()),
);
}
unit(id: number): UnitView {
unit(id: number): UnitView | undefined {
return this._units.get(id);
}
unitInfo(type: UnitType): UnitInfo {
+13 -16
View File
@@ -76,7 +76,7 @@ export class PlayerImpl implements Player {
public _units: UnitImpl[] = [];
public _tiles: Set<TileRef> = new Set();
private _flag: string;
private _flag: string | undefined;
private _name: string;
private _displayName: string;
@@ -127,7 +127,7 @@ export class PlayerImpl implements Player {
name: this.name(),
displayName: this.displayName(),
id: this.id(),
team: this.team(),
team: this.team() ?? undefined,
smallID: this.smallID(),
playerType: this.type(),
isAlive: this.isAlive(),
@@ -172,7 +172,7 @@ export class PlayerImpl implements Player {
return this._smallID;
}
flag(): string {
flag(): string | undefined {
return this._flag;
}
@@ -183,7 +183,7 @@ export class PlayerImpl implements Player {
return this._displayName;
}
clientID(): ClientID {
clientID(): ClientID | null {
return this.playerInfo.clientID;
}
@@ -331,8 +331,10 @@ export class PlayerImpl implements Player {
if (other == this) {
return null;
}
return this.alliances().find(
(a) => a.recipient() == other || a.requestor() == other,
return (
this.alliances().find(
(a) => a.recipient() == other || a.requestor() == other,
) ?? null
);
}
@@ -375,7 +377,7 @@ export class PlayerImpl implements Player {
return this.isTraitor_;
}
createAllianceRequest(recipient: Player): AllianceRequest {
createAllianceRequest(recipient: Player): AllianceRequest | null {
if (this.isAlliedWith(recipient)) {
throw new Error(`cannot create alliance request, already allies`);
}
@@ -386,10 +388,8 @@ export class PlayerImpl implements Player {
if (other == this) {
throw new Error(`cannot get relation with self: ${this}`);
}
if (this.relations.has(other)) {
return this.relationFromValue(this.relations.get(other));
}
return Relation.Neutral;
const relation = this.relations.get(other) ?? 0;
return this.relationFromValue(relation);
}
private relationFromValue(relationValue: number): Relation {
@@ -418,10 +418,7 @@ export class PlayerImpl implements Player {
if (other == this) {
throw new Error(`cannot update relation with self: ${this}`);
}
let relation = 0;
if (this.relations.has(other)) {
relation = this.relations.get(other);
}
const relation = this.relations.get(other) ?? 0;
const newRelation = within(relation + delta, -100, 100);
this.relations.set(other, newRelation);
}
@@ -953,7 +950,7 @@ export class PlayerImpl implements Player {
createAttack(
target: Player | TerraNullius,
troops: number,
sourceTile: TileRef,
sourceTile: TileRef | null,
): Attack {
const attack = new AttackImpl(
this._pseudo_random.nextID(),
+5 -1
View File
@@ -2,7 +2,11 @@ import { AllPlayersStats, PlayerStats } from "../Schemas";
import { NukeType, PlayerID } from "./Game";
export interface Stats {
increaseNukeCount(sender: PlayerID, target: PlayerID, type: NukeType): void;
increaseNukeCount(
sender: PlayerID,
target: PlayerID | null,
type: NukeType,
): void;
getPlayerStats(player: PlayerID): PlayerStats;
stats(): AllPlayersStats;
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { ClientID } from "../Schemas";
import { PlayerID, TerraNullius } from "./Game";
import { TerraNullius } from "./Game";
export class TerraNulliusImpl implements TerraNullius {
constructor() {}
@@ -10,7 +10,7 @@ export class TerraNulliusImpl implements TerraNullius {
return "TERRA_NULLIUS_CLIENT_ID";
}
id(): PlayerID {
id() {
return null;
}
+2 -3
View File
@@ -25,9 +25,8 @@ export interface Nation {
export async function loadTerrainMap(
map: GameMapType,
): Promise<TerrainMapData> {
if (loadedMaps.has(map)) {
return loadedMaps.get(map);
}
const cached = loadedMaps.get(map);
if (typeof cached !== "undefined") return cached;
const mapFiles = await terrainMapFileLoader.getMapData(map);
const gameMap = await genTerrainFromBin(mapFiles.mapBin);
+27 -23
View File
@@ -16,19 +16,19 @@ import { PlayerImpl } from "./PlayerImpl";
export class UnitImpl implements Unit {
private _active = true;
private _health: bigint;
private _lastTile: TileRef = null;
private _lastTile: TileRef | null = null;
// Currently only warship use it
private _target: Unit = null;
private _moveTarget: TileRef = null;
private _target: Unit | null = null;
private _moveTarget: TileRef | null = null;
private _targetedBySAM = false;
private _constructionType: UnitType = undefined;
private _constructionType: UnitType | undefined = undefined;
private _cooldownTick: Tick | null = null;
private _dstPort: Unit | null = null; // Only for trade ships
private _detonationDst: TileRef | null = null; // Only for nukes
private _warshipTarget: Unit | null = null;
private _cooldownDuration: number | null = null;
private _dstPort: Unit | undefined = undefined; // Only for trade ships
private _detonationDst: TileRef | undefined = undefined; // Only for nukes
private _warshipTarget: Unit | undefined = undefined;
private _cooldownDuration: number | undefined = undefined;
constructor(
private _type: UnitType,
@@ -54,6 +54,11 @@ export class UnitImpl implements Unit {
toUpdate(): UnitUpdate {
const warshipTarget = this.warshipTarget();
const dstPort = this.dstPort();
if (this._lastTile === null) throw new Error("null _lastTile");
const ticksLeftInCooldown =
typeof this._cooldownDuration !== "undefined"
? this.ticksLeftInCooldown(this._cooldownDuration)
: undefined;
return {
type: GameUpdateType.Unit,
unitType: this._type,
@@ -65,10 +70,10 @@ export class UnitImpl implements Unit {
lastPos: this._lastTile,
health: this.hasHealth() ? Number(this._health) : undefined,
constructionType: this._constructionType,
dstPortId: dstPort ? dstPort.id() : null,
warshipTargetId: warshipTarget ? warshipTarget.id() : null,
detonationDst: this.detonationDst(),
ticksLeftInCooldown: this.ticksLeftInCooldown(this._cooldownDuration),
dstPortId: dstPort?.id() ?? undefined,
warshipTargetId: warshipTarget?.id() ?? undefined,
detonationDst: this.detonationDst() ?? undefined,
ticksLeftInCooldown,
};
}
@@ -77,6 +82,7 @@ export class UnitImpl implements Unit {
}
lastTile(): TileRef {
if (this._lastTile === null) throw new Error("null _lastTile");
return this._lastTile;
}
@@ -157,7 +163,7 @@ export class UnitImpl implements Unit {
if (this.type() != UnitType.Construction) {
throw new Error(`Cannot get construction type on ${this.type()}`);
}
return this._constructionType;
return this._constructionType ?? null;
}
setConstructionType(type: UnitType): void {
@@ -180,16 +186,16 @@ export class UnitImpl implements Unit {
this._warshipTarget = target;
}
warshipTarget(): Unit {
return this._warshipTarget;
warshipTarget(): Unit | null {
return this._warshipTarget ?? null;
}
detonationDst(): TileRef {
return this._detonationDst;
detonationDst(): TileRef | null {
return this._detonationDst ?? null;
}
dstPort(): Unit {
return this._dstPort;
dstPort(): Unit | null {
return this._dstPort ?? null;
}
// set the cooldown to the current tick or remove it
@@ -204,10 +210,8 @@ export class UnitImpl implements Unit {
}
ticksLeftInCooldown(cooldownDuration: number): Tick {
return Math.max(
0,
cooldownDuration - (this.mg.ticks() - this._cooldownTick),
);
const cooldownTick = this._cooldownTick ?? 0;
return Math.max(0, cooldownDuration - (this.mg.ticks() - cooldownTick));
}
isCooldown(): boolean {
+3 -1
View File
@@ -1,11 +1,13 @@
export class UserSettings {
get(key: string, defaultValue: boolean) {
get(key: string, defaultValue: boolean): boolean {
const value = localStorage.getItem(key);
if (!value) return defaultValue;
if (value === "true") return true;
if (value === "false") return false;
return defaultValue;
}
set(key: string, value: boolean) {
+10 -4
View File
@@ -5,9 +5,9 @@ import { AStar, PathFindResultType, TileResult } from "./AStar";
import { MiniAStar } from "./MiniAStar";
export class PathFinder {
private curr: TileRef = null;
private dst: TileRef = null;
private path: TileRef[];
private curr: TileRef | null = null;
private dst: TileRef | null = null;
private path: TileRef[] | null = null;
private aStar: AStar;
private computeFinished = true;
@@ -63,7 +63,11 @@ export class PathFinder {
this.computeFinished = false;
return this.nextTile(curr, dst);
} else {
return { type: PathFindResultType.NextTile, tile: this.path.shift() };
const tile = this.path?.shift();
if (typeof tile === "undefined") {
throw new Error("missing tile");
}
return { type: PathFindResultType.NextTile, tile };
}
}
@@ -79,6 +83,8 @@ export class PathFinder {
return { type: PathFindResultType.Pending };
case PathFindResultType.PathNotFound:
return { type: PathFindResultType.PathNotFound };
default:
throw new Error("unexpected compute result");
}
}
+3 -2
View File
@@ -119,8 +119,9 @@ export class SerialAStar implements AStar {
1.1 * Math.abs(this.gameMap.x(a) - this.gameMap.x(b)) +
Math.abs(this.gameMap.y(a) - this.gameMap.y(b))
);
} catch {
consolex.log("uh oh");
} catch (e) {
consolex.log("uh oh", e);
return 0;
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
switch (message.type) {
case "heartbeat":
(await gameRunner).executeNextTick();
(await gameRunner)?.executeNextTick();
break;
case "init":
try {
+27 -28
View File
@@ -419,33 +419,32 @@ function getThumbnailColor(t: Terrain): {
return { r: 204, g: 203, b: 158, a: 255 };
}
let adjRGB: number;
switch (true) {
//plains
case t.magnitude < 10:
adjRGB = 220 - 2 * t.magnitude;
return {
r: 190,
g: adjRGB,
b: 138,
a: 255,
};
//highlands
case t.magnitude < 20:
adjRGB = 2 * t.magnitude;
return {
r: 200 + adjRGB,
g: 183 + adjRGB,
b: 138 + adjRGB,
a: 255,
};
//mountains
case t.magnitude >= 20:
adjRGB = Math.floor(230 + t.magnitude / 2);
return {
r: adjRGB,
g: adjRGB,
b: adjRGB,
a: 255,
};
if (t.magnitude < 10) {
// Plains
adjRGB = 220 - 2 * t.magnitude;
return {
r: 190,
g: adjRGB,
b: 138,
a: 255,
};
} else if (t.magnitude < 20) {
// Highlands
adjRGB = 2 * t.magnitude;
return {
r: 200 + adjRGB,
g: 183 + adjRGB,
b: 138 + adjRGB,
a: 255,
};
} else {
// Mountains
adjRGB = Math.floor(230 + t.magnitude / 2);
return {
r: adjRGB,
g: adjRGB,
b: adjRGB,
a: 255,
};
}
}
+1
View File
@@ -126,6 +126,7 @@ export async function readGameRecord(
Key: `${gameFolder}/${gameId}`, // Fixed - needed to include gameFolder
});
// Parse the response body
if (typeof response.Body === "undefined") return null;
const bodyContents = await response.Body.transformToString();
return JSON.parse(bodyContents) as GameRecord;
} catch (error) {
+1 -1
View File
@@ -16,7 +16,7 @@ export class GameManager {
}
public game(id: GameID): GameServer | null {
return this.games.get(id);
return this.games.get(id) ?? null;
}
addClient(client: Client, gameID: GameID, lastTurn: number): boolean {
+22 -22
View File
@@ -42,13 +42,13 @@ export class GameServer {
// Used for record record keeping
private allClients: Map<ClientID, Client> = new Map();
private _hasStarted = false;
private _startTime: number = null;
private _startTime: number | null = null;
private endTurnIntervalID;
private lastPingUpdate = 0;
private winner: ClientSendWinnerMessage = null;
private winner: ClientSendWinnerMessage | null = null;
// This field is currently only filled at victory
private allPlayersStats: AllPlayersStats = {};
@@ -68,32 +68,32 @@ export class GameServer {
this.log = log_.child({ gameID: id });
}
public updateGameConfig(gameConfig: GameConfig): void {
if (gameConfig.gameMap != null) {
public updateGameConfig(gameConfig: Partial<GameConfig>): void {
if (typeof gameConfig.gameMap !== "undefined") {
this.gameConfig.gameMap = gameConfig.gameMap;
}
if (gameConfig.difficulty != null) {
if (typeof gameConfig.difficulty !== "undefined") {
this.gameConfig.difficulty = gameConfig.difficulty;
}
if (gameConfig.disableNPCs != null) {
if (typeof gameConfig.disableNPCs !== "undefined") {
this.gameConfig.disableNPCs = gameConfig.disableNPCs;
}
if (gameConfig.disableNukes != null) {
if (typeof gameConfig.disableNukes !== "undefined") {
this.gameConfig.disableNukes = gameConfig.disableNukes;
}
if (gameConfig.bots != null) {
if (typeof gameConfig.bots !== "undefined") {
this.gameConfig.bots = gameConfig.bots;
}
if (gameConfig.infiniteGold != null) {
if (typeof gameConfig.infiniteGold !== "undefined") {
this.gameConfig.infiniteGold = gameConfig.infiniteGold;
}
if (gameConfig.infiniteTroops != null) {
if (typeof gameConfig.infiniteTroops !== "undefined") {
this.gameConfig.infiniteTroops = gameConfig.infiniteTroops;
}
if (gameConfig.instantBuild != null) {
if (typeof gameConfig.instantBuild !== "undefined") {
this.gameConfig.instantBuild = gameConfig.instantBuild;
}
if (gameConfig.gameMode != null) {
if (typeof gameConfig.gameMode !== "undefined") {
this.gameConfig.gameMode = gameConfig.gameMode;
}
}
@@ -145,17 +145,17 @@ export class GameServer {
"message",
gatekeeper.wsHandler(client.ip, async (message: string) => {
try {
let clientMsg: ClientMessage = null;
let clientMsg: ClientMessage | null = null;
try {
clientMsg = ClientMessageSchema.parse(JSON.parse(message));
} catch (error) {
throw Error(`error parsing schema for ${client.ip}`);
}
if (this.allClients.has(clientMsg.clientID)) {
const client = this.allClients.get(clientMsg.clientID);
if (client.persistentID != clientMsg.persistentID) {
const c = this.allClients.get(clientMsg.clientID);
if (typeof c !== "undefined") {
if (c.persistentID != clientMsg.persistentID) {
this.log.warn(
`Client ID ${clientMsg.clientID} sent incorrect id ${clientMsg.persistentID}, does not match persistent id ${client.persistentID}`,
`Client ID ${clientMsg.clientID} sent incorrect id ${clientMsg.persistentID}, does not match persistent id ${c.persistentID}`,
);
return;
}
@@ -214,7 +214,7 @@ export class GameServer {
}
public startTime(): number {
if (this._startTime > 0) {
if (this._startTime !== null && this._startTime > 0) {
return this._startTime;
} else {
//game hasn't started yet, only works for public games
@@ -368,10 +368,10 @@ export class GameServer {
this.gameStartInfo,
playerRecords,
this.turns,
this._startTime,
this._startTime ?? 0,
Date.now(),
this.winner?.winner,
this.winner?.winnerType,
this.winner?.winner ?? null,
this.winner?.winnerType ?? null,
this.allPlayersStats,
),
);
@@ -405,7 +405,7 @@ export class GameServer {
phase(): GamePhase {
const now = Date.now();
const alive = [];
const alive: Client[] = [];
for (const client of this.activeClients) {
if (now - client.lastPing > 60_000) {
this.log.info(
+1 -1
View File
@@ -26,7 +26,7 @@ export interface Gatekeeper {
) => (message: string) => Promise<void>;
}
let gk: Gatekeeper = null;
let gk: Gatekeeper | null = null;
async function getGatekeeperCached(): Promise<Gatekeeper> {
if (gk != null) {
+1 -1
View File
@@ -60,7 +60,7 @@ export class MapPlaylist {
case 1:
this.currentPlaylistCounter++;
return PlaylistType.BigMaps;
case 2:
default:
this.currentPlaylistCounter = 0;
return PlaylistType.SmallMaps;
}
+20 -2
View File
@@ -170,7 +170,7 @@ app.get(
);
async function fetchLobbies(): Promise<number> {
const fetchPromises = [];
const fetchPromises: Promise<GameInfo | null>[] = [];
for (const gameID of publicLobbyIDs) {
const controller = new AbortController();
@@ -209,8 +209,26 @@ async function fetchLobbies(): Promise<number> {
});
lobbyInfos.forEach((l) => {
if (l.msUntilStart <= 250 || l.gameConfig.maxPlayers <= l.numClients) {
if (
"msUntilStart" in l &&
typeof l.msUntilStart !== "undefined" &&
l.msUntilStart <= 250
) {
publicLobbyIDs.delete(l.gameID);
return;
}
if (
"gameConfig" in l &&
typeof l.gameConfig !== "undefined" &&
"maxPlayers" in l.gameConfig &&
typeof l.gameConfig.maxPlayers !== "undefined" &&
"numClients" in l &&
typeof l.numClients !== "undefined" &&
l.gameConfig.maxPlayers <= l.numClients
) {
publicLobbyIDs.delete(l.gameID);
return;
}
});
+2 -2
View File
@@ -29,8 +29,8 @@ export function setupMetricsServer() {
// Track seen metric names to avoid duplicate metadata
const seenMetrics = new Set();
const processedLines = [];
const allMetricValues = [];
const processedLines: string[] = [];
const allMetricValues: string[] = [];
// Process all metadata information in the master metrics first
const masterLines = masterMetrics.split("\n");
+1 -1
View File
@@ -15,7 +15,7 @@ let game: Game;
let attacker: Player;
function attackerBuildsNuke(
source: TileRef,
source: TileRef | null,
target: TileRef,
initialize = true,
) {
+4
View File
@@ -58,6 +58,10 @@ describe("Warship", () => {
test("Warship heals only if player has port", async () => {
const maxHealth = game.config().unitInfo(UnitType.Warship).maxHealth;
if (typeof maxHealth !== "number") {
expect(typeof maxHealth).toBe("number");
throw new Error("unreachable");
}
const port = player1.buildUnit(UnitType.Port, 0, game.ref(coastX, 10));
const warship = player1.buildUnit(
+14 -4
View File
@@ -1,6 +1,11 @@
import fs from "fs/promises";
import path from "path";
import { Difficulty, GameType } from "../../src/core/game/Game";
import {
Difficulty,
GameMapType,
GameMode,
GameType,
} from "../../src/core/game/Game";
import { createGame } from "../../src/core/game/GameImpl";
import { genTerrainFromBin } from "../../src/core/game/TerrainMapLoader";
import { UserSettings } from "../../src/core/game/UserSettings";
@@ -9,7 +14,10 @@ import { generateMap } from "../../src/scripts/TerrainMapGenerator";
import { TestConfig } from "./TestConfig";
import { TestServerConfig } from "./TestServerConfig";
export async function setup(mapName: string, _gameConfig: GameConfig = {}) {
export async function setup(
mapName: string,
_gameConfig: Partial<GameConfig> = {},
) {
// Load the specified map
const mapPath = path.join(__dirname, "..", "testdata", `${mapName}.png`);
const imageBuffer = await fs.readFile(mapPath);
@@ -22,11 +30,13 @@ export async function setup(mapName: string, _gameConfig: GameConfig = {}) {
// Configure the game
const serverConfig = new TestServerConfig();
const gameConfig = {
gameMap: null,
const gameConfig: GameConfig = {
gameMap: GameMapType.Asia,
gameMode: GameMode.FFA,
gameType: GameType.Singleplayer,
difficulty: Difficulty.Medium,
disableNPCs: false,
disableNukes: false,
bots: 0,
infiniteGold: false,
infiniteTroops: false,
+1
View File
@@ -19,6 +19,7 @@
"esModuleInterop": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"strictNullChecks": true,
"useDefineForClassFields": false
},
"include": [