mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:50:43 +00:00
better UX for boats
This commit is contained in:
@@ -53,12 +53,10 @@
|
||||
* REFACTOR: use new priority queue DONE 8/27/2024
|
||||
* BUG: players attack each other same time creates islands DONE 8/28/2024
|
||||
* make bot spawn better DONE 8/28/2024
|
||||
* make UX for attacking TerraNullius by boat better
|
||||
* make bots more likely to attack weaker players
|
||||
* make UX for attacking TerraNullius by boat better DONE 8/29/2024
|
||||
* make bot territory less funky (more likely attack neighbors with larger border)
|
||||
- maybe use bounding box, bots want to be circular?
|
||||
* PERF: use hierarchical a* search for boats
|
||||
* precompute spawns
|
||||
* PERF: precompute spawns
|
||||
* end game when no players left (or after 1 hour or so?)
|
||||
* boats can go around the world
|
||||
* Add terrain elevation to map
|
||||
@@ -66,4 +64,4 @@
|
||||
* REFACTOR: give terranullius an ID, game.player() returns terranullius
|
||||
* REFACTOR: ocean is considered TerraNullius ?
|
||||
* REFACTOR: remove player config?
|
||||
* PERF: render tiles more efficiently
|
||||
* Make fake humans
|
||||
|
||||
+44
-21
@@ -7,7 +7,7 @@ import {GameRenderer} from "./graphics/GameRenderer";
|
||||
import {InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent} from "./InputHandler"
|
||||
import {ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, ClientMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn} from "../core/Schemas";
|
||||
import {TerrainMap} from "../core/TerrainMapLoader";
|
||||
import {bfs, manhattanDist} from "../core/Util";
|
||||
import {and, bfs, dist, manhattanDist} from "../core/Util";
|
||||
import {TerrainRenderer} from "./graphics/TerrainRenderer";
|
||||
|
||||
|
||||
@@ -203,28 +203,12 @@ export class ClientGame {
|
||||
|
||||
const owner = tile.owner()
|
||||
const targetID = owner.isPlayer() ? owner.id() : null;
|
||||
let tn: Tile[] = []
|
||||
if (tile.owner() != this.myPlayer) {
|
||||
|
||||
// Boat Attack Terra Nullius
|
||||
if (tile.isLand()) {
|
||||
tn = Array.from(bfs(tile, 2))
|
||||
.filter(t => t.isOcean())
|
||||
.sort((a, b) => manhattanDist(tile.cell(), a.cell()) - manhattanDist(tile.cell(), b.cell()))
|
||||
.flatMap(t => t.neighbors())
|
||||
.filter(n => n.isShore())
|
||||
.filter(n => !n.hasOwner())
|
||||
} else if (tile.isOcean()) {
|
||||
tn = Array.from(bfs(tile, 3))
|
||||
.filter(t => t.isShore())
|
||||
.filter(t => !t.hasOwner())
|
||||
.sort((a, b) => manhattanDist(tile.cell(), a.cell()) - manhattanDist(tile.cell(), b.cell()))
|
||||
}
|
||||
if (tn.length > 0) {
|
||||
this.sendBoatAttackIntent(targetID, tn[0].cell(), this.gs.config().player().boatAttackAmount(this.myPlayer, owner))
|
||||
return
|
||||
}
|
||||
if (tile.owner() == this.myPlayer) {
|
||||
return
|
||||
}
|
||||
|
||||
if (tile.hasOwner()) {
|
||||
// Attack Player
|
||||
if (tile.isLand()) {
|
||||
if (this.myPlayer.sharesBorderWith(tile.owner())) {
|
||||
@@ -234,6 +218,45 @@ export class ClientGame {
|
||||
this.sendBoatAttackIntent(targetID, cell, this.gs.config().player().boatAttackAmount(this.myPlayer, owner))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Attack Terra Nullius
|
||||
if (tile.isLand()) {
|
||||
|
||||
const neighbors = Array.from(bfs(tile, and((r, t) => t.isLand(), dist(100))));
|
||||
for (const n of neighbors) {
|
||||
if (this.myPlayer.borderTiles().has(n)) {
|
||||
this.sendAttackIntent(targetID, cell, this.gs.config().player().attackAmount(this.myPlayer, owner))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const tn = Array.from(bfs(tile, dist(30)))
|
||||
.filter(t => t.isOceanShore())
|
||||
.filter(t => !t.hasOwner())
|
||||
.sort((a, b) => manhattanDist(tile.cell(), a.cell()) - manhattanDist(tile.cell(), b.cell()))
|
||||
if (tn.length > 0) {
|
||||
this.sendBoatAttackIntent(targetID, tn[0].cell(), this.gs.config().player().boatAttackAmount(this.myPlayer, owner))
|
||||
} else {
|
||||
this.sendAttackIntent(targetID, cell, this.gs.config().player().attackAmount(this.myPlayer, owner))
|
||||
}
|
||||
}
|
||||
|
||||
if (tile.isOcean()) {
|
||||
const bordersOcean = Array.from(this.myPlayer.borderTiles()).filter(t => t.isOceanShore()).length > 0
|
||||
if (!bordersOcean) {
|
||||
return
|
||||
}
|
||||
const tn = Array.from(bfs(tile, dist(3)))
|
||||
.filter(t => t.isOceanShore())
|
||||
.filter(t => !t.hasOwner())
|
||||
.sort((a, b) => manhattanDist(tile.cell(), a.cell()) - manhattanDist(tile.cell(), b.cell()))
|
||||
if (tn.length > 0) {
|
||||
this.sendBoatAttackIntent(targetID, tn[0].cell(), this.gs.config().player().boatAttackAmount(this.myPlayer, owner))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import {Cell, Game, PlayerEvent, Tile, TileEvent, Player, Execution, BoatEvent}
|
||||
import {Theme} from "../../core/configuration/Config";
|
||||
import {DragEvent, ZoomEvent} from "../InputHandler";
|
||||
import {NameRenderer} from "./NameRenderer";
|
||||
import {bfs, manhattanDist} from "../../core/Util";
|
||||
import {bfs, dist, manhattanDist} from "../../core/Util";
|
||||
import {PseudoRandom} from "../../core/PseudoRandom";
|
||||
import {TerrainRenderer} from "./TerrainRenderer";
|
||||
import {PriorityQueue} from "@datastructures-js/priority-queue";
|
||||
@@ -157,11 +157,10 @@ export class GameRenderer {
|
||||
}
|
||||
|
||||
boatEvent(event: BoatEvent) {
|
||||
bfs(event.oldTile, 2).forEach(t => this.paintTerritory(t))
|
||||
bfs(event.oldTile, dist(2)).forEach(t => this.paintTerritory(t))
|
||||
if (event.boat.isActive()) {
|
||||
bfs(event.boat.tile(), 2).forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.boat.owner().id())))
|
||||
bfs(event.boat.tile(), 1).forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.boat.owner().id())))
|
||||
|
||||
bfs(event.boat.tile(), dist(2)).forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.boat.owner().id())))
|
||||
bfs(event.boat.tile(), dist(1)).forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.boat.owner().id())))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export class PlayerInfo {
|
||||
export interface Tile {
|
||||
isLand(): boolean
|
||||
isShore(): boolean
|
||||
isOceanShore(): boolean
|
||||
isWater(): boolean
|
||||
isShorelineWater(): boolean
|
||||
isOcean(): boolean
|
||||
|
||||
@@ -35,6 +35,10 @@ class TileImpl implements Tile {
|
||||
isShore(): boolean {
|
||||
return this.isLand() && this._terrain.shoreline
|
||||
}
|
||||
isOceanShore(): boolean {
|
||||
return this.isShore() && this.neighbors().find(t => t.isOcean()) != null
|
||||
}
|
||||
|
||||
isShorelineWater(): boolean {
|
||||
return this.isWater() && this._terrain.shoreline
|
||||
}
|
||||
|
||||
+11
-2
@@ -1,3 +1,4 @@
|
||||
import {functional} from "typia";
|
||||
import {Cell, Tile} from "./Game";
|
||||
|
||||
export function manhattanDist(c1: Cell, c2: Cell): number {
|
||||
@@ -8,7 +9,15 @@ export function within(value: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
export function bfs(tile: Tile, dist: number): Set<Tile> {
|
||||
export function dist(dist: number): (root: Tile, tile: Tile) => boolean {
|
||||
return (root: Tile, n: Tile) => manhattanDist(root.cell(), n.cell()) <= dist;
|
||||
}
|
||||
|
||||
export function and(x: (root: Tile, tile: Tile) => boolean, y: (root: Tile, tile: Tile) => boolean): (root: Tile, tile: Tile) => boolean {
|
||||
return (root: Tile, tile: Tile) => x(root, tile) && y(root, tile)
|
||||
}
|
||||
|
||||
export function bfs(tile: Tile, filter: (root: Tile, tile: Tile) => boolean): Set<Tile> {
|
||||
const seen = new Set<Tile>
|
||||
const q: Tile[] = []
|
||||
q.push(tile)
|
||||
@@ -16,7 +25,7 @@ export function bfs(tile: Tile, dist: number): Set<Tile> {
|
||||
const curr = q.pop()
|
||||
seen.add(curr)
|
||||
for (const n of curr.neighbors()) {
|
||||
if (!seen.has(n) && manhattanDist(tile.cell(), n.cell()) <= dist) {
|
||||
if (!seen.has(n) && filter(tile, n)) {
|
||||
q.push(n)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {DefaultConfig, DefaultPlayerConfig, defaultPlayerConfig} from "./Default
|
||||
|
||||
export const devConfig = new class extends DefaultConfig {
|
||||
numSpawnPhaseTurns(): number {
|
||||
return 40
|
||||
return 60
|
||||
}
|
||||
gameCreationRate(): number {
|
||||
return 3 * 1000
|
||||
@@ -19,7 +19,7 @@ export const devConfig = new class extends DefaultConfig {
|
||||
return devPlayerConfig
|
||||
}
|
||||
numBots(): number {
|
||||
return 250
|
||||
return 50
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ export class AStar {
|
||||
compute(iterations: number): boolean {
|
||||
if (this.completed) return true;
|
||||
|
||||
while (!this.openSet.size()) {
|
||||
while (!this.openSet.isEmpty()) {
|
||||
iterations--
|
||||
this.current = this.openSet.dequeue()!.tile;
|
||||
if (iterations <= 0) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Cell, Game} from "../Game";
|
||||
import {PseudoRandom} from "../PseudoRandom";
|
||||
import {SpawnIntent} from "../Schemas";
|
||||
import {bfs} from "../Util";
|
||||
import {bfs, dist as dist} from "../Util";
|
||||
import {getSpawnCells} from "./Util";
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export class BotSpawner {
|
||||
spawnBot(botName: string): SpawnIntent {
|
||||
const rand = this.random.nextInt(0, this.numFreeTiles);
|
||||
const spawn = this.freeTiles[rand];
|
||||
bfs(this.gs.tile(spawn), 50).forEach(t => this.removeCell(t.cell()))
|
||||
bfs(this.gs.tile(spawn), dist(50)).forEach(t => this.removeCell(t.cell()))
|
||||
const spawnIntent: SpawnIntent = {
|
||||
type: 'spawn',
|
||||
name: botName,
|
||||
|
||||
Reference in New Issue
Block a user