import { v4 as uuidv4 } from "uuid"; import twemoji from "twemoji"; import DOMPurify from "dompurify"; import { Cell, Game, Player, Unit } from "./game/Game"; import { AllPlayersStats, ClientID, GameConfig, GameID, GameRecord, PlayerRecord, PlayerStats, Turn, } from "./Schemas"; import { customAlphabet, nanoid } from "nanoid"; import { andFN, GameMap, manhattanDistFN, TileRef } from "./game/GameMap"; export function manhattanDistWrapped( c1: Cell, c2: Cell, width: number, ): number { // Calculate x distance let dx = Math.abs(c1.x - c2.x); // Check if wrapping around the x-axis is shorter dx = Math.min(dx, width - dx); // Calculate y distance (no wrapping for y-axis) const dy = Math.abs(c1.y - c2.y); // Return the sum of x and y distances return dx + dy; } export function within(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } export function distSort( gm: GameMap, target: TileRef, ): (a: TileRef, b: TileRef) => number { return (a: TileRef, b: TileRef) => { return gm.manhattanDist(a, target) - gm.manhattanDist(b, target); }; } export function distSortUnit( gm: GameMap, target: Unit | TileRef, ): (a: Unit, b: Unit) => number { const targetRef = typeof target === "number" ? target : target.tile(); return (a: Unit, b: Unit) => { return ( gm.manhattanDist(a.tile(), targetRef) - gm.manhattanDist(b.tile(), targetRef) ); }; } // TODO: refactor to new file export function sourceDstOceanShore( gm: Game, src: Player, tile: TileRef, ): [TileRef | null, TileRef | null] { const dst = gm.owner(tile); const srcTile = closestShoreFromPlayer(gm, src, tile); let dstTile: TileRef | null = null; if (dst.isPlayer()) { dstTile = closestShoreFromPlayer(gm, dst as Player, tile); } else { dstTile = closestShoreTN(gm, tile, 50); } return [srcTile, dstTile]; } export function targetTransportTile(gm: Game, tile: TileRef): TileRef | null { const dst = gm.playerBySmallID(gm.ownerID(tile)); let dstTile: TileRef | null = null; if (dst.isPlayer()) { dstTile = closestShoreFromPlayer(gm, dst as Player, tile); } else { dstTile = closestShoreTN(gm, tile, 50); } return dstTile; } export function closestShoreFromPlayer( gm: GameMap, player: Player, target: TileRef, ): TileRef | null { const shoreTiles = Array.from(player.borderTiles()).filter((t) => gm.isShore(t), ); if (shoreTiles.length == 0) { return null; } return shoreTiles.reduce((closest, current) => { const closestDistance = manhattanDistWrapped( gm.cell(target), gm.cell(closest), gm.width(), ); const currentDistance = manhattanDistWrapped( gm.cell(target), gm.cell(current), gm.width(), ); return currentDistance < closestDistance ? current : closest; }); } function closestShoreTN( gm: GameMap, tile: TileRef, searchDist: number, ): TileRef { const tn = Array.from( gm.bfs( tile, andFN((_, t) => !gm.hasOwner(t), manhattanDistFN(tile, searchDist)), ), ) .filter((t) => gm.isShore(t)) .sort((a, b) => gm.manhattanDist(tile, a) - gm.manhattanDist(tile, b)); if (tn.length == 0) { return null; } return tn[0]; } export function simpleHash(str: string): number { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash); } export function calculateBoundingBox( gm: GameMap, borderTiles: ReadonlySet, ): { min: Cell; max: Cell } { let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; borderTiles.forEach((tile: TileRef) => { const cell = gm.cell(tile); minX = Math.min(minX, cell.x); minY = Math.min(minY, cell.y); maxX = Math.max(maxX, cell.x); maxY = Math.max(maxY, cell.y); }); return { min: new Cell(minX, minY), max: new Cell(maxX, maxY) }; } export function calculateBoundingBoxCenter( gm: GameMap, borderTiles: ReadonlySet, ): Cell { const { min, max } = calculateBoundingBox(gm, borderTiles); return new Cell( min.x + Math.floor((max.x - min.x) / 2), min.y + Math.floor((max.y - min.y) / 2), ); } export function inscribed( outer: { min: Cell; max: Cell }, inner: { min: Cell; max: Cell }, ): boolean { return ( outer.min.x <= inner.min.x && outer.min.y <= inner.min.y && outer.max.x >= inner.max.x && outer.max.y >= inner.max.y ); } export function getMode(list: Set): number { // Count occurrences const counts = new Map(); for (const item of list) { counts.set(item, (counts.get(item) || 0) + 1); } // Find the item with the highest count let mode = 0; let maxCount = 0; for (const [item, count] of counts) { if (count > maxCount) { maxCount = count; mode = item; } } return mode; } export function sanitize(name: string): string { return Array.from(name) .join("") .replace(/[^\p{L}\p{N}\s\p{Emoji}\p{Emoji_Component}\[\]_]/gu, ""); } export function processName(name: string): string { // First sanitize the raw input - strip everything except text and emojis const sanitizedName = sanitize(name); // Process emojis with twemoji const withEmojis = twemoji.parse(sanitizedName, { base: "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/", folder: "svg", ext: ".svg", }); // Add CSS styles inline to the wrapper span const styledHTML = ` ${withEmojis} `; // Add CSS for the emoji images const withEmojiStyles = styledHTML.replace( / b ? a : b; } export function minInt(a: bigint, b: bigint): bigint { return a < b ? a : b; } export function withinInt(num: bigint, min: bigint, max: bigint): bigint { const atLeastMin = maxInt(num, min); return minInt(atLeastMin, max); }