diff --git a/API.md b/API.md new file mode 100644 index 000000000..0dda65425 --- /dev/null +++ b/API.md @@ -0,0 +1,94 @@ +## API Usage + +### List Game Metadata + +Get game IDs and basic metadata for games that started within a specified time range. Results are sorted by start time and paginated. + +**Constraints:** + +- Maximum time range: 2 days +- Maximum limit per request: 1000 games + +**Endpoint:** + +``` +GET https://api.openfront.io/public/games +``` + +**Query Parameters:** + +- `start` (required): ISO 8601 timestamp +- `end` (required): ISO 8601 timestamp +- `type` (optional): Game type, must be one of `[Private, Public, Singleplayer]` +- `limit` (optional): Number of results (max 1000, default 50) +- `offset` (optional): Pagination offset + +**Example Request:** + +```bash +curl "https://api.openfront.io/public/games?start=2025-10-25T00:00:00Z&end=2025-10-26T23:59:59Z&type=Singleplayer&limit=10&offset=5" +``` + +**Response:** + +```json +[ + { + "game": "ABSgwin6", + "start": "2025-10-25T00:00:10.526Z", + "end": "2025-10-25T00:19:45.187Z", + "type": "Singleplayer", + "mode": "Free For All", + "difficulty": "Medium" + }, + ... +] +``` + +The response includes a `Content-Range` header indicating pagination (e.g., `games 5-15/399`). + +--- + +### Get Game Info + +Retrieve detailed information about a specific game. + +**Endpoint:** + +``` +GET https://api.openfront.io/public/game/:gameId +``` + +**Query Parameters:** + +- `turns` (optional): Set to `false` to exclude turn data and reduce response size + +**Examples:** + +```bash +# Full game data +curl "https://api.openfront.io/public/game/ABSgwin6" + +# Without turn data +curl "https://api.openfront.io/public/game/ABSgwin6?turns=false" +``` + +**Note:** Public player IDs are stripped from game records for privacy. + +--- + +### Get Player Info + +Retrieve information and stats for a specific player. + +**Endpoint:** + +``` +GET https://api.openfront.io/public/player/:playerId +``` + +**Example:** + +```bash +curl "https://api.openfront.io/public/player/HabCsQYR" +``` diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts index 3c186be1f..35cd183e6 100644 --- a/src/client/PublicLobby.ts +++ b/src/client/PublicLobby.ts @@ -17,6 +17,7 @@ export class PublicLobby extends LitElement { private currLobby: GameInfo | null = null; private debounceDelay: number = 750; private lobbyIDToStart = new Map(); + private lobbiesFetchInFlight: Promise | null = null; createRenderRoot() { return this; @@ -73,16 +74,26 @@ export class PublicLobby extends LitElement { } async fetchLobbies(): Promise { - try { - const response = await fetch(`/api/public_lobbies`); - if (!response.ok) - throw new Error(`HTTP error! status: ${response.status}`); - const data = await response.json(); - return data.lobbies; - } catch (error) { - console.error("Error fetching lobbies:", error); - throw error; + if (this.lobbiesFetchInFlight) { + return this.lobbiesFetchInFlight; } + + this.lobbiesFetchInFlight = (async () => { + try { + const response = await fetch(`/api/public_lobbies`); + if (!response.ok) + throw new Error(`HTTP error! status: ${response.status}`); + const data = await response.json(); + return data.lobbies as GameInfo[]; + } catch (error) { + console.error("Error fetching lobbies:", error); + throw error; + } finally { + this.lobbiesFetchInFlight = null; + } + })(); + + return this.lobbiesFetchInFlight; } public stop() { diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index 945ac3524..c8ba09200 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -2,7 +2,12 @@ import { PriorityQueue } from "@datastructures-js/priority-queue"; import { Colord } from "colord"; import { Theme } from "../../../core/configuration/Config"; import { EventBus } from "../../../core/EventBus"; -import { Cell, PlayerType, UnitType } from "../../../core/game/Game"; +import { + Cell, + ColoredTeams, + PlayerType, + UnitType, +} from "../../../core/game/Game"; import { euclDistFN, TileRef } from "../../../core/game/GameMap"; import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameView, PlayerView } from "../../../core/game/GameView"; @@ -170,6 +175,7 @@ export class TerritoryLayer implements Layer { .filter((p) => p.type() === PlayerType.Human); const focusedPlayer = this.game.focusedPlayer(); + const teamColors = Object.values(ColoredTeams); for (const human of humans) { if (human === focusedPlayer) { continue; @@ -191,7 +197,17 @@ export class TerritoryLayer implements Layer { // In Team games, the spawn highlight color becomes that player's team color // Optionally, this could be broken down to teammate or enemy and simplified to green and red, respectively const team = human.team(); - if (team !== null) color = this.theme.teamColor(team); + if (team !== null) { + if (teamColors.includes(team)) { + color = this.theme.teamColor(team); + } else { + if (myPlayer.isFriendly(human)) { + color = this.theme.spawnHighlightTeamColor(); + } else { + color = this.theme.spawnHighlightColor(); + } + } + } } for (const tile of this.game.bfs( diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index bc2491a72..a8cc3fb42 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -132,12 +132,8 @@ export class PlayerExecution implements Execution { private surroundedBySamePlayer(cluster: Set): false | Player { const enemies = new Set(); for (const tile of cluster) { - const isOceanShore = this.mg.isOceanShore(tile); - if (this.mg.isOceanShore(tile) && !isOceanShore) { - continue; - } if ( - isOceanShore || + this.mg.isOceanShore(tile) || this.mg.isOnEdgeOfMap(tile) || this.mg.neighbors(tile).some((n) => !this.mg?.hasOwner(n)) ) { diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index fce214059..bf25de30c 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -935,6 +935,9 @@ export class PlayerImpl implements Player { if (!this.isAlive()) { return false; } + if (unit.owner() !== this) { + return false; + } return true; }