Enable the @typescript-eslint/no-non-null-assertion eslint rule (#1899)

## Description:

Enable the `@typescript-eslint/no-non-null-assertion` eslint rule.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
This commit is contained in:
Scott Anderson
2025-08-23 13:50:41 -04:00
committed by GitHub
parent add81b9c04
commit f5316cc378
35 changed files with 188 additions and 127 deletions
+2
View File
@@ -79,6 +79,7 @@ export default [
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-mixed-enums": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/prefer-as-const": "error",
@@ -143,6 +144,7 @@ export default [
rules: {
// Disabled rules for tests, configs
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
+2 -6
View File
@@ -452,11 +452,7 @@ export class ClientGameRunner {
return;
}
this.findAndUpgradeNearestBuilding(tile);
}
private findAndUpgradeNearestBuilding(clickedTile: TileRef) {
this.myPlayer!.actions(clickedTile).then((actions) => {
this.myPlayer.actions(tile).then((actions) => {
const upgradeUnits: {
unitId: number;
unitType: UnitType;
@@ -470,7 +466,7 @@ export class ClientGameRunner {
.find((unit) => unit.id() === bu.canUpgrade);
if (existingUnit) {
const distance = this.gameView.manhattanDist(
clickedTile,
tile,
existingUnit.tile(),
);
+4 -4
View File
@@ -169,7 +169,8 @@ export class TerritoryPatternsModal extends LitElement {
text-white text-xs font-medium rounded transition-colors"
@click=${(e: Event) => {
e.stopPropagation();
handlePurchase(pattern.product!.priceId);
if (!pattern.product) return;
handlePurchase(pattern.product.priceId);
}}
>
${translateText("territory_patterns.purchase")}
@@ -362,9 +363,8 @@ export function generatePreviewDataUrl(
): string {
pattern ??= DEFAULT_PATTERN_B64;
if (patternCache.has(pattern)) {
return patternCache.get(pattern)!;
}
const cached = patternCache.get(pattern);
if (cached !== undefined) return cached;
// Calculate canvas size
const decoder = new PatternDecoder(pattern, base64url.decode);
+1 -1
View File
@@ -582,7 +582,7 @@ export class Transport {
} else {
console.log(
"WebSocket is not open. Current state:",
this.socket!.readyState,
this.socket?.readyState,
);
console.log("attempting reconnect");
}
+5 -5
View File
@@ -154,7 +154,9 @@ export class AnimatedSpriteLoader {
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext("2d")!.drawImage(img, 0, 0);
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("2D context not supported");
ctx.drawImage(img, 0, 0);
this.animatedSpriteImageMap.set(typedFxType, canvas);
} catch (err) {
@@ -192,10 +194,8 @@ export class AnimatedSpriteLoader {
const borderColor = theme.borderColor(owner);
const spawnHighlightColor = theme.spawnHighlightColor();
const key = `${fxType}-${owner.id()}`;
let coloredCanvas: HTMLCanvasElement;
if (this.coloredAnimatedSpriteCache.has(key)) {
coloredCanvas = this.coloredAnimatedSpriteCache.get(key)!;
} else {
let coloredCanvas = this.coloredAnimatedSpriteCache.get(key);
if (coloredCanvas === undefined) {
coloredCanvas = colorizeCanvas(
baseImage,
territoryColor,
+3 -1
View File
@@ -143,7 +143,9 @@ export function largestRectangleInHistogram(widths: number[]): Rectangle {
const h = i === widths.length ? 0 : widths[i];
while (stack.length > 0 && h < widths[stack[stack.length - 1]]) {
const height = widths[stack.pop()!];
const lastIndex = stack.pop();
if (lastIndex === undefined) break; // cannot happen due to the while guard
const height = widths[lastIndex];
const width = stack.length === 0 ? i : i - stack[stack.length - 1] - 1;
if (height * width > maxArea) {
+4 -4
View File
@@ -120,7 +120,8 @@ export const colorizeCanvas = (
canvas.width = source.width;
canvas.height = source.height;
const ctx = canvas.getContext("2d")!;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("2D context not supported");
ctx.drawImage(source, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
@@ -177,9 +178,8 @@ export const getColoredSprite = (
const borderColor: Colord = customBorderColor ?? theme.borderColor(owner);
const spawnHighlightColor = theme.spawnHighlightColor();
const key = computeSpriteKey(unit, territoryColor, borderColor);
if (coloredSpriteCache.has(key)) {
return coloredSpriteCache.get(key)!;
}
const cached = coloredSpriteCache.get(key);
if (cached !== undefined) return cached;
const sprite = getSpriteForUnit(unit);
if (sprite === null) {
+2 -1
View File
@@ -189,8 +189,9 @@ export class ChatModal extends LitElement {
}
private selectPhrase(phrase: QuickChatPhrase) {
if (this.selectedCategory === null) return;
this.selectedQuickChatKey = this.getFullQuickChatKey(
this.selectedCategory!,
this.selectedCategory,
phrase.key,
);
this.selectedPhraseTemplate = translateText(
@@ -43,7 +43,10 @@ export class GameLeftSidebar extends LitElement implements Layer {
tick() {
if (!this.playerTeam && this.game.myPlayer()?.team()) {
this.playerTeam = this.game.myPlayer()!.team();
const myPlayer = this.game.myPlayer();
if (myPlayer !== null) {
this.playerTeam = myPlayer.team();
}
if (this.playerTeam) {
this.playerColor = this.game
.config()
+1 -1
View File
@@ -1,6 +1,6 @@
export type Layer = {
init?: () => void;
tick?: () => void;
tick?: () => void | Promise<void>;
renderLayer?: (context: CanvasRenderingContext2D) => void;
shouldTransform?: () => boolean;
redraw?: () => void;
+34 -25
View File
@@ -72,7 +72,7 @@ export class MainRadialMenu extends LitElement implements Layer {
init() {
this.radialMenu.init();
this.eventBus.on(ContextMenuEvent, (event) => {
this.eventBus.on(ContextMenuEvent, async (event) => {
const worldCoords = this.transformHandler.screenToWorldCoordinates(
event.x,
event.y,
@@ -80,22 +80,26 @@ export class MainRadialMenu extends LitElement implements Layer {
if (!this.game.isValidCoord(worldCoords.x, worldCoords.y)) {
return;
}
if (this.game.myPlayer() === null) {
const myPlayer = this.game.myPlayer();
if (myPlayer === null) {
return;
}
this.clickedTile = this.game.ref(worldCoords.x, worldCoords.y);
this.game
.myPlayer()!
.actions(this.clickedTile)
.then((actions) => {
this.updatePlayerActions(
this.game.myPlayer()!,
actions,
this.clickedTile!,
event.x,
event.y,
);
});
const tile = this.game.ref(worldCoords.x, worldCoords.y);
this.clickedTile = tile;
try {
const actions = await myPlayer.actions(tile);
// Stale check: user might have clicked somewhere else already
if (this.clickedTile !== tile) return;
this.updatePlayerActions(
myPlayer,
actions,
tile,
event.x,
event.y,
);
} catch (err) {
console.error("Failed to fetch player actions:", err);
}
});
}
@@ -141,16 +145,21 @@ export class MainRadialMenu extends LitElement implements Layer {
async tick() {
if (!this.radialMenu.isMenuVisible() || this.clickedTile === null) return;
if (this.game.ticks() % 5 === 0) {
this.game
.myPlayer()!
.actions(this.clickedTile)
.then((actions) => {
this.updatePlayerActions(
this.game.myPlayer()!,
actions,
this.clickedTile!,
);
});
const myPlayer = this.game.myPlayer();
if (myPlayer === null) return;
const tile = this.clickedTile;
if (tile === null) return;
try {
const actions = await myPlayer.actions(tile);
if (this.clickedTile !== tile) return; // stale
this.updatePlayerActions(
myPlayer,
actions,
tile,
);
} catch (err) {
console.error("Failed to refresh player actions:", err);
}
}
}
@@ -237,7 +237,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
${ref((el) => {
if (el instanceof HTMLElement) {
requestAnimationFrame(() => {
renderPlayerFlag(player.cosmetics.flag!, el);
renderPlayerFlag(player.cosmetics.flag, el);
});
}
})}
+6 -1
View File
@@ -552,6 +552,7 @@ export class RadialMenu implements Layer {
} else {
content
.append("image")
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
.attr("xlink:href", d.data.icon!)
.attr("width", this.config.iconSize)
.attr("height", this.config.iconSize)
@@ -936,7 +937,11 @@ export class RadialMenu implements Layer {
this.currentLevel = 0;
this.menuStack = [];
this.currentMenuItems = this.rootMenu.subMenu!(this.params!);
if (this.rootMenu.subMenu === undefined || this.params === null) {
this.currentMenuItems = [];
} else {
this.currentMenuItems = this.rootMenu.subMenu(this.params);
}
this.navigationInProgress = false;
@@ -105,6 +105,7 @@ export enum Slot {
Delete = "delete",
}
/* eslint-disable @typescript-eslint/no-non-null-assertion */
const infoChatElement: MenuElement = {
id: "info_chat",
name: "chat",
@@ -292,6 +293,7 @@ const infoEmojiElement: MenuElement = {
return emojiElements;
},
};
/* eslint-enable @typescript-eslint/no-non-null-assertion */
export const infoMenuElement: MenuElement = {
id: Slot.Info,
+2 -1
View File
@@ -46,7 +46,8 @@ export class ReplayPanel extends LitElement implements Layer {
tick() {
if (!this.visible) return;
if (this.game!.ticks() % 10 === 0) {
if (!this.game) return;
if (this.game.ticks() % 10 === 0) {
this.requestUpdate();
}
}
@@ -232,8 +232,9 @@ export class StructureIconsLayer implements Layer {
private modifyVisibility(render: StructureRenderInfo) {
const structureType =
render.unit.type() === UnitType.Construction
? render.unit.constructionType()!
? render.unit.constructionType()
: render.unit.type();
if (structureType === undefined) return;
const structureInfos = this.structures.get(structureType);
let focusStructure = false;
@@ -341,14 +342,14 @@ export class StructureIconsLayer implements Layer {
);
return PIXI.Texture.EMPTY;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const structureType = isConstruction ? constructionType! : unit.type();
const cacheKey = isConstruction
? `construction-${structureType}` + (renderIcon ? "-icon" : "")
: `${this.theme.territoryColor(unit.owner()).toRgbString()}-${structureType}` +
(renderIcon ? "-icon" : "");
if (this.textureCache.has(cacheKey)) {
return this.textureCache.get(cacheKey)!;
}
const cached = this.textureCache.get(cacheKey);
if (cached !== undefined) return cached;
const shape = STRUCTURE_SHAPES[structureType];
const texture = shape
@@ -379,7 +380,8 @@ export class StructureIconsLayer implements Layer {
}
structureCanvas.width = Math.ceil(iconSize);
structureCanvas.height = Math.ceil(iconSize);
const context = structureCanvas.getContext("2d")!;
const context = structureCanvas.getContext("2d");
if (!context) throw new Error("2D context not supported");
let borderColor: string;
if (isConstruction) {
@@ -561,7 +563,8 @@ export class StructureIconsLayer implements Layer {
unit.type() === UnitType.Construction
? unit.constructionType()
: unit.type();
const shape = STRUCTURE_SHAPES[unitType!];
const shape =
unitType !== undefined ? STRUCTURE_SHAPES[unitType] : undefined;
if (shape !== undefined) {
text.position.y = Math.round(-ICON_SIZE[shape] / 2 - 2);
}
@@ -598,7 +601,8 @@ export class StructureIconsLayer implements Layer {
const imageCanvas = document.createElement("canvas");
imageCanvas.width = image.width;
imageCanvas.height = image.height;
const ctx = imageCanvas.getContext("2d")!;
const ctx = imageCanvas.getContext("2d");
if (!ctx) throw new Error("2D context not supported");
ctx.fillStyle = color;
ctx.fillRect(0, 0, imageCanvas.width, imageCanvas.height);
ctx.globalCompositeOperation = "destination-in";
+2 -2
View File
@@ -14,7 +14,7 @@ const ANIMATION_DURATIONS: Record<string, number> = {
// TODO: Pass in cosmetics as a parameter when
// remote cosmetics are implemented for custom flags
export function renderPlayerFlag(
flag: string,
flag: string | undefined,
target: HTMLElement,
cosmetics: Cosmetics | undefined = undefined,
) {
@@ -23,7 +23,7 @@ export function renderPlayerFlag(
return;
}
if (!flag.startsWith("!")) return;
if (!flag?.startsWith("!")) return;
const code = flag.slice("!".length);
const layers = code.split("_").map((segment) => {
+4 -3
View File
@@ -23,10 +23,11 @@ export class EventBus {
eventType: EventConstructor<T>,
callback: (event: T) => void,
): void {
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, []);
let callbacks = this.listeners.get(eventType);
if (callbacks === undefined) {
callbacks = [];
this.listeners.set(eventType, callbacks);
}
const callbacks = this.listeners.get(eventType)!;
callbacks.push(callback as (event: GameEvent) => void);
}
+4 -6
View File
@@ -53,9 +53,8 @@ export class ColorAllocator {
}
assignColor(id: string): Colord {
if (this.assigned.has(id)) {
return this.assigned.get(id)!;
}
const cached = this.assigned.get(id);
if (cached !== undefined) return cached;
if (this.availableColors.length === 0) {
this.availableColors = [...this.fallbackColors];
@@ -87,9 +86,8 @@ export class ColorAllocator {
}
assignTeamPlayerColor(team: Team, playerId: string): Colord {
if (this.teamPlayerColors.has(playerId)) {
return this.teamPlayerColors.get(playerId)!;
}
const cached = this.teamPlayerColors.get(playerId);
if (cached !== undefined) return cached;
const teamColors = this.getTeamColorVariations(team);
const hashValue = simpleHash(playerId);
+3 -3
View File
@@ -84,9 +84,9 @@ export class PastelTheme implements Theme {
}
borderColor(player: PlayerView): Colord {
if (this.borderColorCache.has(player.id())) {
return this.borderColorCache.get(player.id())!;
}
const cached = this.borderColorCache.get(player.id());
if (cached !== undefined) return cached;
const tc = this.territoryColor(player).rgba;
/* eslint-disable sort-keys */
const color = colord({
+3 -3
View File
@@ -81,9 +81,9 @@ export class PastelThemeDark implements Theme {
}
borderColor(player: PlayerView): Colord {
if (this.borderColorCache.has(player.id())) {
return this.borderColorCache.get(player.id())!;
}
const cached = this.borderColorCache.get(player.id());
if (cached !== undefined) return cached;
const tc = this.territoryColor(player).rgba;
const color = colord({
r: Math.max(tc.r - 40, 0),
+2 -1
View File
@@ -214,8 +214,9 @@ export class NukeExecution implements Execution {
const targetRangeSquared =
this.mg.config().defaultNukeTargetableRange() ** 2;
const targetTile = this.nuke.targetTile();
if (targetTile === undefined) return;
this.nuke.setTargetable(
this.isTargetable(targetTile!, this.nuke.tile(), targetRangeSquared),
this.isTargetable(targetTile, this.nuke.tile(), targetRangeSquared),
);
}
+2 -1
View File
@@ -79,7 +79,8 @@ export class PortExecution implements Execution {
shouldSpawnTradeShip(): boolean {
const numTradeShips = this.mg.unitCount(UnitType.TradeShip);
const spawnRate = this.mg.config().tradeShipSpawnRate(numTradeShips);
for (let i = 0; i < this.port!.level(); i++) {
const level = this.port?.level() ?? 0;
for (let i = 0; i < level; i++) {
if (this.random.chance(spawnRate)) {
return true;
}
+5 -4
View File
@@ -131,21 +131,22 @@ export class TradeShipExecution implements Execution {
}
private complete() {
if (this.tradeShip === undefined) throw new Error("Not initialized");
this.active = false;
this.tradeShip!.delete(false);
this.tradeShip.delete(false);
const gold = this.mg
.config()
.tradeShipGold(
this.tilesTraveled,
this.tradeShip!.owner().unitCount(UnitType.Port),
this.tradeShip.owner().unitCount(UnitType.Port),
);
if (this.wasCaptured) {
this.tradeShip!.owner().addGold(gold, this._dstPort.tile());
this.tradeShip.owner().addGold(gold, this._dstPort.tile());
this.mg.displayMessage(
`Received ${renderNumber(gold)} gold from ship captured from ${this.origOwner.displayName()}`,
MessageType.CAPTURED_ENEMY_UNIT,
this.tradeShip!.owner().id(),
this.tradeShip.owner().id(),
gold,
);
} else {
@@ -168,6 +168,7 @@ export class TransportShipExecution implements Execution {
this.lastMove = ticks;
if (this.boat.retreating()) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.dst = this.src!; // src is guaranteed to be set at this point
}
+23 -14
View File
@@ -102,9 +102,11 @@ export class WarshipExecution implements Execution {
) {
continue;
}
const patrolTile = this.warship.patrolTile();
if (
patrolTile !== undefined &&
this.mg.euclideanDistSquared(
this.warship.patrolTile()!,
patrolTile,
unit.tile(),
) > patrolRangeSquared
) {
@@ -150,9 +152,11 @@ export class WarshipExecution implements Execution {
}
private shootTarget() {
const targetUnit = this.warship.targetUnit();
if (targetUnit === undefined) return;
const shellAttackRate = this.mg.config().warshipShellAttackRate();
if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) {
if (this.warship.targetUnit()?.type() !== UnitType.TransportShip) {
if (targetUnit?.type() !== UnitType.TransportShip) {
// Warships don't need to reload when attacking transport ships.
this.lastShellAttack = this.mg.ticks();
}
@@ -161,12 +165,12 @@ export class WarshipExecution implements Execution {
this.warship.tile(),
this.warship.owner(),
this.warship,
this.warship.targetUnit()!,
targetUnit,
),
);
if (!this.warship.targetUnit()!.hasHealth()) {
if (!targetUnit.hasHealth()) {
// Don't send multiple shells to target that can be oneshotted
this.alreadySentShell.add(this.warship.targetUnit()!);
this.alreadySentShell.add(targetUnit);
this.warship.setTargetUnit(undefined);
return;
}
@@ -174,16 +178,18 @@ export class WarshipExecution implements Execution {
}
private huntDownTradeShip() {
const targetUnit = this.warship.targetUnit();
if (targetUnit === undefined) return;
for (let i = 0; i < 2; i++) {
// target is trade ship so capture it.
const result = this.pathfinder.nextTile(
this.warship.tile(),
this.warship.targetUnit()!.tile(),
targetUnit.tile(),
5,
);
switch (result.type) {
case PathFindResultType.Completed:
this.warship.owner().captureUnit(this.warship.targetUnit()!);
this.warship.owner().captureUnit(targetUnit);
this.warship.setTargetUnit(undefined);
this.warship.move(this.warship.tile());
return;
@@ -201,16 +207,17 @@ export class WarshipExecution implements Execution {
}
private patrol() {
if (this.warship.targetTile() === undefined) {
this.warship.setTargetTile(this.randomTile());
if (this.warship.targetTile() === undefined) {
let targetTile = this.warship.targetTile();
if (targetTile === undefined) {
targetTile = this.randomTile();
this.warship.setTargetTile(targetTile);
if (targetTile === undefined) {
return;
}
}
const result = this.pathfinder.nextTile(
this.warship.tile(),
this.warship.targetTile()!,
targetTile,
);
switch (result.type) {
case PathFindResultType.Completed:
@@ -243,12 +250,14 @@ export class WarshipExecution implements Execution {
const maxAttemptBeforeExpand = 500;
let attempts = 0;
let expandCount = 0;
const patrolTile = this.warship.patrolTile();
if (patrolTile === undefined) return;
while (expandCount < 3) {
const x =
this.mg.x(this.warship.patrolTile()!) +
this.mg.x(patrolTile) +
this.random.nextInt(-warshipPatrolRange / 2, warshipPatrolRange / 2);
const y =
this.mg.y(this.warship.patrolTile()!) +
this.mg.y(patrolTile) +
this.random.nextInt(-warshipPatrolRange / 2, warshipPatrolRange / 2);
if (!this.mg.isValidCoord(x, y)) {
continue;
+2 -1
View File
@@ -104,7 +104,8 @@ export class AttackImpl implements Attack {
return null;
}
// No border tiles yet—use the source tile's location
const tile: number = this.sourceTile()!;
const tile = this.sourceTile();
if (tile === null) return null;
return new Cell(this._mg.map().x(tile), this._mg.map().y(tile));
}
+3 -2
View File
@@ -651,8 +651,9 @@ export class GameImpl implements Game {
"team",
winner,
...this.players()
.filter((p) => p.team() === winner && p.clientID() !== null)
.map((p) => p.clientID()!),
.filter((p) => p.team() === winner)
.map((p) => p.clientID())
.filter((id): id is ClientID => id !== null),
];
} else {
const clientId = winner.clientID();
+6 -4
View File
@@ -220,8 +220,9 @@ export class RailNetworkImpl implements RailNetwork {
{ station: start, distance: 0 },
];
while (queue.length > 0) {
const { station, distance } = queue.shift()!;
let head = 0;
while (head < queue.length) {
const { station, distance } = queue[head++];
if (visited.has(station)) continue;
visited.add(station);
@@ -244,8 +245,9 @@ export class RailNetworkImpl implements RailNetwork {
const visited = new Set<TrainStation>();
const queue = [start];
while (queue.length > 0) {
const current = queue.shift()!;
let head = 0;
while (head < queue.length) {
const current = queue[head++];
if (visited.has(current)) continue;
visited.add(current);
+5 -3
View File
@@ -16,10 +16,12 @@ export function assignTeams(
// Sort players into clan groups or no-clan list
for (const player of players) {
if (player.clan) {
if (!clanGroups.has(player.clan)) {
clanGroups.set(player.clan, []);
let group = clanGroups.get(player.clan);
if (group === undefined) {
group = [];
clanGroups.set(player.clan, group);
}
clanGroups.get(player.clan)!.push(player);
group.push(player);
} else {
noClanPlayers.push(player);
}
+14 -7
View File
@@ -87,6 +87,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
}
// Process forward search
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const fwdCurrent = this.fwdOpenSet.poll()!.tile;
// Check if we've found a meeting point
@@ -98,6 +99,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
this.expandNode(fwdCurrent, true);
// Process backward search
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const bwdCurrent = this.bwdOpenSet.poll()!.tile;
// Check if we've found a meeting point
@@ -126,7 +128,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
const openSet = isForward ? this.fwdOpenSet : this.bwdOpenSet;
const cameFrom = isForward ? this.fwdCameFrom : this.bwdCameFrom;
const tentativeGScore = gScore.get(current)! + this.graph.cost(neighbor);
const tentativeGScore = (gScore.get(current) ?? 0) + this.graph.cost(neighbor);
let penalty = 0;
// With a direction change penalty, the path will get as straight as possible
if (this.directionChangePenalty > 0) {
@@ -141,7 +143,8 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
}
const totalG = tentativeGScore + penalty;
if (!gScore.has(neighbor) || totalG < gScore.get(neighbor)!) {
const g = gScore.get(neighbor);
if (g === undefined || totalG < g) {
cameFrom.set(neighbor, current);
gScore.set(neighbor, totalG);
const fScore =
@@ -172,19 +175,23 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
// Reconstruct path from start to meeting point
const fwdPath: NodeType[] = [this.meetingPoint];
let current = this.meetingPoint;
let current: NodeType = this.meetingPoint;
while (this.fwdCameFrom.has(current)) {
current = this.fwdCameFrom.get(current)!;
let f = this.fwdCameFrom.get(current);
while (f !== undefined) {
current = f;
fwdPath.unshift(current);
f = this.fwdCameFrom.get(current);
}
// Reconstruct path from meeting point to goal
current = this.meetingPoint;
while (this.bwdCameFrom.has(current)) {
current = this.bwdCameFrom.get(current)!;
let b = this.bwdCameFrom.get(current);
while (b !== undefined) {
current = b;
fwdPath.push(current);
b = this.bwdCameFrom.get(current);
}
return fwdPath;
+7 -3
View File
@@ -44,13 +44,17 @@ export class WorkerClient {
break;
case "initialized":
default:
if (message.id && this.messageHandlers.has(message.id)) {
const handler = this.messageHandlers.get(message.id)!;
default: {
if (message.id === undefined) return;
const handler = this.messageHandlers.get(message.id);
if (handler === undefined) return;
try {
handler(message);
} finally {
this.messageHandlers.delete(message.id);
}
break;
}
}
}
+4 -4
View File
@@ -662,8 +662,8 @@ export class GameServer {
// Count occurrences of each hash
for (const client of this.activeClients) {
if (client.hashes.has(turnNumber)) {
const clientHash = client.hashes.get(turnNumber)!;
const clientHash = client.hashes.get(turnNumber);
if (clientHash !== undefined) {
counts.set(clientHash, (counts.get(clientHash) ?? 0) + 1);
}
}
@@ -683,8 +683,8 @@ export class GameServer {
let outOfSyncClients: Client[] = [];
for (const client of this.activeClients) {
if (client.hashes.has(turnNumber)) {
const clientHash = client.hashes.get(turnNumber)!;
const clientHash = client.hashes.get(turnNumber);
if (clientHash !== undefined) {
if (clientHash !== mostCommonHash) {
outOfSyncClients.push(client);
}
+10 -4
View File
@@ -103,14 +103,20 @@ export class MapPlaylist {
const numAttempts = 10000;
for (let i = 0; i < numAttempts; i++) {
if (this.shuffleMapsPlaylist()) {
log.info(`Generated map playlist in ${i} attempts`);
return this.mapsPlaylist.shift()!;
log.info(`Generated map playlist in ${i + 1} attempts`);
const next = this.mapsPlaylist.shift();
if (next !== undefined) return next;
log.error("Playlist unexpectedly empty after successful shuffle; using fallback.");
return { map: GameMapType.World, mode: GameMode.FFA };
}
}
log.error("Failed to generate a valid map playlist");
}
// Even if it failed, playlist will be partially populated.
return this.mapsPlaylist.shift()!;
// Even if it failed, playlist may be partially populated.
const fallback = this.mapsPlaylist.shift();
if (fallback !== undefined) return fallback;
log.error("Playlist empty after shuffle failure; using fallback.");
return { map: GameMapType.World, mode: GameMode.FFA };
}
private shuffleMapsPlaylist(): boolean {
@@ -131,10 +131,11 @@ function handleWinner(
// Add client vote
const winnerKey = JSON.stringify(clientMsg.winner);
if (!gs.winnerVotes.has(winnerKey)) {
gs.winnerVotes.set(winnerKey, { ips: new Set(), winner: clientMsg });
let potentialWinner = gs.winnerVotes.get(winnerKey);
if (potentialWinner === undefined) {
potentialWinner = { ips: new Set(), winner: clientMsg };
gs.winnerVotes.set(winnerKey, potentialWinner);
}
const potentialWinner = gs.winnerVotes.get(winnerKey)!;
potentialWinner.ips.add(client.ip);
const activeUniqueIPs = new Set(gs.activeClients.map((c) => c.ip));