can change spawn in beginning of game

This commit is contained in:
evanpelle
2024-08-25 20:21:35 -07:00
parent 1d7c4c996f
commit 51650eb930
17 changed files with 187 additions and 43 deletions
+6 -5
View File
@@ -43,15 +43,16 @@
* BUG: boat doesn't work if on lake if other player not on same lake DONE 8/23/2024
* Allow boats to attack TerraNullius DONE 8/23/2024
* try vintage theme DONE 8/24/2023
* Make lobby background the terrain map
* improve menu (keep highlighted when click, allow deselect lobby)
* BUG: fix hotreload (priority queue breaks it) DONE 8/24/2024
* improve menu (keep highlighted when click, allow deselect lobby) DONE 8/25/2024
* give time to (re) spawn at start of game
* store & delay tile updates for lag compensation
* add shader to dim border
* REFACTOR: remove player.info()
* REFACTOR: give terranullius an ID, game.player() returns terranullius
* give time to (re) spawn at start of game
* store & delay tile updates for lag compensation
* REFACTOR: ocean is considered TerraNullius ?
* BUG: fix hotreload (priority queue breaks it)
* REFACTOR: remove player config?
* PERF: use hierarchical a* search for boats
* PERF: render tiles more efficiently
* Add terrain elevation to map
* boats can go around the world
+2 -2
View File
@@ -182,12 +182,12 @@ export class ClientGame {
return
}
const tile = this.gs.tile(cell)
if (!tile.hasOwner() && !this.spawned && this.myPlayer == null) {
if (tile.isLand() && !tile.hasOwner() && this.gs.inSpawnPhase()) {
this.sendSpawnIntent(cell)
this.spawned = true
return
}
if (!this.spawned || this.myPlayer == null) {
if (this.gs.inSpawnPhase()) {
return
}
+21
View File
@@ -20,6 +20,27 @@
<div id="player-count" class="player-count"></div>
</button>
</div>
<div id="dev-log">
<h4>DEVLOG: 8/18/2024 - 8/25/2024</h4>
<ul id="dev-log">
<li>Create dev server at <a href="https://www.openfront.dev">openfront.dev</a></li>
<li>fix invert zoom</li>
<li>better algorithm for name render placement</li>
<li>show how many players in each lobby</li>
<li>make boats larger</li>
<li>boats same color as owner</li>
<li>make coasts look better</li>
<li>have boats not get close to shore</li>
<li>improve terrain colors</li>
<li>FIXED: boat doesn't work if on lake if other player not on same lake</li>
<li>Allow boats to attack unowned land</li>
<li>add vintage theme</li>
<li>Created Panama Canal and Bosphorus Strait</li>
<li>Make lobby background the terrain map</li>
<li>Created favicon</li>
<li>improve menu (keep highlighted when click, allow deselect lobby)</li>
</ul>
</div>
</div>
<style>
+8 -2
View File
@@ -2,7 +2,7 @@ import {Config} from "./configuration/Config"
import {GameEvent} from "./EventBus"
import {ClientID, GameID} from "./Schemas"
export type PlayerID = number // TODO: make string?
export type PlayerID = string
export class Cell {
@@ -21,6 +21,7 @@ export class Cell {
export interface ExecutionView {
isActive(): boolean
owner(): Player
activeDuringSpawnPhase(): boolean
}
export interface Execution extends ExecutionView {
@@ -34,7 +35,8 @@ export class PlayerInfo {
public readonly name: string,
public readonly isBot: boolean,
// null if bot.
public readonly clientID: ClientID | null
public readonly clientID: ClientID | null,
public readonly id: PlayerID
) { }
}
@@ -101,6 +103,7 @@ export interface MutablePlayer extends Player {
addTroops(troops: number): void
removeTroops(troops: number): void
conquer(tile: Tile): void
relinquish(tile: Tile): void
executions(): Execution[]
neighbors(): (MutablePlayer | TerraNullius)[]
boats(): MutableBoat[]
@@ -110,6 +113,7 @@ export interface MutablePlayer extends Player {
export interface Game {
// Throws exception is player not found
player(id: PlayerID): Player
hasPlayer(id: PlayerID): boolean
players(): Player[]
tile(cell: Cell): Tile
isOnMap(cell: Cell): boolean
@@ -120,6 +124,8 @@ export interface Game {
executions(): ExecutionView[]
terraNullius(): TerraNullius
tick(): void
ticks(): number
inSpawnPhase(): boolean
addExecution(...exec: Execution[]): void
config(): Config
}
+83 -20
View File
@@ -2,6 +2,7 @@ import {Config} from "./configuration/Config";
import {EventBus} from "./EventBus";
import {Cell, Execution, MutableGame, Game, MutablePlayer, PlayerEvent, PlayerID, PlayerInfo, Player, TerraNullius, Tile, TileEvent, Boat, MutableBoat, BoatEvent} from "./Game";
import {Terrain, TerrainMap, TerrainType} from "./TerrainMapLoader";
import {simpleHash} from "./Util";
export function createGame(terrainMap: TerrainMap, eventBus: EventBus, config: Config): Game {
return new GameImpl(terrainMap, eventBus, config)
@@ -122,7 +123,7 @@ export class PlayerImpl implements MutablePlayer {
public _boats: BoatImpl[] = []
public _tiles: Map<CellString, Tile> = new Map<CellString, Tile>()
constructor(private gs: GameImpl, public readonly _id: PlayerID, public readonly playerInfo: PlayerInfo, private _troops) {
constructor(private gs: GameImpl, public readonly playerInfo: PlayerInfo, private _troops) {
}
addBoat(troops: number, tile: Tile, target: Player | TerraNullius): BoatImpl {
@@ -134,7 +135,6 @@ export class PlayerImpl implements MutablePlayer {
boats(): BoatImpl[] {
return this._boats
}
sharesBorderWith(other: Player | TerraNullius): boolean {
@@ -183,8 +183,14 @@ export class PlayerImpl implements MutablePlayer {
ownsTile(cell: Cell): boolean {return this._tiles.has(cell.toString())}
setTroops(troops: number) {this._troops = Math.floor(troops)}
conquer(tile: Tile) {this.gs.conquer(this, tile)}
relinquish(tile: Tile) {
if (tile.owner() != this) {
throw new Error(`Cannot relinquish tile not owned by this player`)
}
this.gs.relinquish(tile)
}
info(): PlayerInfo {return this.playerInfo}
id(): PlayerID {return this._id}
id(): PlayerID {return this.playerInfo.id}
troops(): number {return this._troops}
isAlive(): boolean {return this._tiles.size > 0}
gameState(): MutableGame {return this.gs}
@@ -192,7 +198,7 @@ export class PlayerImpl implements MutablePlayer {
return this.gs.executions().filter(exec => exec.owner().id() == this.id())
}
hash(): number {
return this.id() * (this.troops() + this.numTilesOwned())
return simpleHash(this.id()) * (this.troops() + this.numTilesOwned())
}
toString(): string {
return `Player:{name:${this.info().name},clientID:${this.info().clientID},isAlive:${this.isAlive()},troops:${this._troops},numTileOwned:${this.numTilesOwned()}}]`
@@ -207,22 +213,21 @@ class TerraNulliusImpl implements TerraNullius {
}
id(): PlayerID {
return 0
return 'TerraNulliusID'
}
ownsTile(cell: Cell): boolean {
return this.tiles.has(cell)
}
isPlayer(): false {return false as const}
}
export class GameImpl implements MutableGame {
private ticks = 0
private _ticks = 0
private unInitExecs: Execution[] = []
idCounter: PlayerID = 1; // Zero reserved for TerraNullius
// idCounter: PlayerID = 1; // Zero reserved for TerraNullius
map: TileImpl[][]
_players: Map<PlayerID, PlayerImpl> = new Map<PlayerID, PlayerImpl>
private execs: Execution[] = []
@@ -243,20 +248,44 @@ export class GameImpl implements MutableGame {
}
}
}
hasPlayer(id: PlayerID): boolean {
return this._players.has(id)
}
config(): Config {
return this._config
}
inSpawnPhase(): boolean {
return this._ticks <= this.config().turnsUntilGameStart()
}
ticks(): number {
return this._ticks
}
tick() {
this.executions().forEach(e => e.tick(this.ticks))
this.unInitExecs.forEach(e => e.init(this, this.ticks))
this.executions().forEach(e => {
if (!this.inSpawnPhase() || e.activeDuringSpawnPhase()) {
e.tick(this._ticks)
}
})
const inited: Execution[] = []
const unInited: Execution[] = []
this.unInitExecs.forEach(e => {
if (!this.inSpawnPhase() || e.activeDuringSpawnPhase()) {
e.init(this, this._ticks)
inited.push(e)
} else {
unInited.push(e)
}
})
this.removeInactiveExecutions()
this.execs.push(...this.unInitExecs)
this.unInitExecs = []
this.ticks++
if (this.ticks % 100 == 0) {
this.execs.push(...inited)
this.unInitExecs = unInited
this._ticks++
if (this._ticks % 100 == 0) {
let hash = 1;
this._players.forEach(p => {
if (!p.info().isBot) {
@@ -264,7 +293,7 @@ export class GameImpl implements MutableGame {
}
hash += p.hash()
})
console.log(`tick ${this.ticks}: hash ${hash}`)
console.log(`tick ${this._ticks}: hash ${hash}`)
}
}
@@ -273,7 +302,23 @@ export class GameImpl implements MutableGame {
}
removeInactiveExecutions(): void {
this.execs = this.execs.filter(e => e.isActive())
const activeExecs: Execution[] = []
for (const exec of this.execs) {
if (this.inSpawnPhase()) {
if (exec.activeDuringSpawnPhase()) {
if (exec.isActive()) {
activeExecs.push(exec)
}
} else {
activeExecs.push(exec)
}
} else {
if (exec.isActive()) {
activeExecs.push(exec)
}
}
}
this.execs = activeExecs
}
players(): MutablePlayer[] {
@@ -313,10 +358,8 @@ export class GameImpl implements MutableGame {
}
addPlayer(playerInfo: PlayerInfo, troops: number): MutablePlayer {
let id = this.idCounter
this.idCounter++
let player = new PlayerImpl(this, id, playerInfo, troops)
this._players.set(id, player)
let player = new PlayerImpl(this, playerInfo, troops)
this._players.set(playerInfo.id, player)
this.eventBus.emit(new PlayerEvent(player))
return player
}
@@ -406,6 +449,26 @@ export class GameImpl implements MutableGame {
this.eventBus.emit(new TileEvent(tile))
}
relinquish(tile: Tile) {
if (!tile.hasOwner()) {
throw new Error(`Cannot relinquish tile because it is unowned: cell ${tile.cell().toString()}`)
}
if (tile.isWater()) {
throw new Error("Cannot relinquish water")
}
const tileImpl = tile as TileImpl
let previousOwner = tileImpl._owner as PlayerImpl
previousOwner._tiles.delete(tile.cell().toString())
previousOwner._borderTiles.delete(tile.cell().toString())
previousOwner._borderTileSet.delete(tile)
tileImpl._isBorder = false
tileImpl._owner = this._terraNullius
this.updateBorders(tile)
this.eventBus.emit(new TileEvent(tile))
}
private updateBorders(tile: Tile) {
const tiles: Tile[] = []
tiles.push(tile)
+4 -4
View File
@@ -37,8 +37,8 @@ const BaseIntentSchema = z.object({
export const AttackIntentSchema = BaseIntentSchema.extend({
type: z.literal('attack'),
attackerID: z.number(),
targetID: z.number().nullable(),
attackerID: z.string(),
targetID: z.string().nullable(),
troops: z.number(),
targetX: z.number(),
targetY: z.number()
@@ -55,8 +55,8 @@ export const SpawnIntentSchema = BaseIntentSchema.extend({
export const BoatAttackIntentSchema = BaseIntentSchema.extend({
type: z.literal('boat'),
attackerID: z.number(),
targetID: z.number().nullable(),
attackerID: z.string(),
targetID: z.string().nullable(),
troops: z.number(),
x: z.number(),
y: z.number(),
+10
View File
@@ -22,4 +22,14 @@ export function bfs(tile: Tile, dist: number): Set<Tile> {
}
}
return seen
}
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);
}
+2 -2
View File
@@ -8,10 +8,10 @@ import {vintageTheme} from "./VintageTheme";
export class DefaultConfig implements Config {
turnsUntilGameStart(): number {
return 25
return 100
}
numBots(): number {
return 500
return 250
}
player(): PlayerConfig {
return defaultPlayerConfig
+6 -3
View File
@@ -3,11 +3,14 @@ import {PlayerConfig} from "./Config";
import {DefaultConfig, DefaultPlayerConfig, defaultPlayerConfig} from "./DefaultConfig";
export const devConfig = new class extends DefaultConfig {
turnsUntilGameStart(): number {
return 100
}
gameCreationRate(): number {
return 21 * 1000
return 3 * 1000
}
lobbyLifetime(): number {
return 20 * 1000
return 3 * 1000
}
turnIntervalMs(): number {
return 100
@@ -25,6 +28,6 @@ export const devPlayerConfig = new class extends DefaultPlayerConfig {
if (playerInfo.isBot) {
return 5000
}
return 10000
return 5000
}
}
+2 -1
View File
@@ -3,6 +3,7 @@ import {PlayerID, Tile} from "../Game";
import {Theme} from "./Config";
import {time} from "console";
import {PseudoRandom} from "../PseudoRandom";
import {simpleHash} from "../Util";
export const pastelTheme = new class implements Theme {
private rand = new PseudoRandom(123)
@@ -71,7 +72,7 @@ export const pastelTheme = new class implements Theme {
}
territoryColor(id: PlayerID): Colord {
return this.territoryColors[id % this.territoryColors.length]
return this.territoryColors[simpleHash(id) % this.territoryColors.length]
}
borderColor(id: PlayerID): Colord {
+2 -1
View File
@@ -1,6 +1,7 @@
import {Colord, colord} from "colord";
import {PlayerID, Tile} from "../Game";
import {Theme} from "./Config";
import {simpleHash} from "../Util";
export const vintageTheme = new class implements Theme {
@@ -72,7 +73,7 @@ export const vintageTheme = new class implements Theme {
}
territoryColor(id: PlayerID): Colord {
return this.territoryColors[id % this.territoryColors.length];
return this.territoryColors[simpleHash(id) % this.territoryColors.length];
}
borderColor(id: PlayerID): Colord {
+4
View File
@@ -24,6 +24,10 @@ export class AttackExecution implements Execution {
private targetCell: Cell | null,
) { }
activeDuringSpawnPhase(): boolean {
return false
}
init(mg: MutableGame, ticks: number) {
// TODO: remove this and fix directed expansion.
@@ -37,6 +37,10 @@ export class BoatAttackExecution implements Execution {
private troops: number,
) { }
activeDuringSpawnPhase(): boolean {
return false
}
init(mg: MutableGame, ticks: number) {
this.lastMove = ticks
+5 -1
View File
@@ -1,6 +1,7 @@
import {Config, PlayerConfig} from "../configuration/Config";
import {Cell, Execution, MutableGame, MutablePlayer, Player, PlayerID, PlayerInfo, TerraNullius} from "../Game"
import {PseudoRandom} from "../PseudoRandom"
import {simpleHash} from "../Util";
import {AttackExecution} from "./AttackExecution";
export class BotExecution implements Execution {
@@ -13,9 +14,12 @@ export class BotExecution implements Execution {
constructor(private bot: MutablePlayer) {
this.random = new PseudoRandom(bot.id())
this.random = new PseudoRandom(simpleHash(bot.id()))
this.attackRate = this.random.nextInt(10, 50)
}
activeDuringSpawnPhase(): boolean {
return false
}
init(mg: MutableGame, ticks: number) {
this.mg = mg
+4 -1
View File
@@ -5,10 +5,13 @@ import {AttackExecution} from "./AttackExecution";
import {SpawnExecution} from "./SpawnExecution";
import {BotSpawner} from "./BotSpawner";
import {BoatAttackExecution} from "./BoatAttackExecution";
import {PseudoRandom} from "../PseudoRandom";
export class Executor {
private random = new PseudoRandom(999)
constructor(private gs: Game) {
}
@@ -27,7 +30,7 @@ export class Executor {
)
} else if (intent.type == "spawn") {
return new SpawnExecution(
new PlayerInfo(intent.name, intent.isBot, intent.clientID),
new PlayerInfo(intent.name, intent.isBot, intent.clientID, this.random.nextID()),
new Cell(intent.x, intent.y)
)
} else if (intent.type == "boat") {
+4
View File
@@ -9,6 +9,10 @@ export class PlayerExecution implements Execution {
constructor(private playerID: PlayerID) {
}
activeDuringSpawnPhase(): boolean {
return false
}
init(mg: MutableGame, ticks: number) {
this.config = mg.config()
this.player = mg.player(this.playerID)
+20 -1
View File
@@ -14,7 +14,6 @@ export class SpawnExecution implements Execution {
private cell: Cell
) { }
init(mg: MutableGame, ticks: number) {
this.mg = mg
}
@@ -23,6 +22,21 @@ export class SpawnExecution implements Execution {
if (!this.isActive()) {
return
}
if (ticks >= this.mg.config().turnsUntilGameStart()) {
this.active = false
return
}
const existing = this.mg.players().find(p => p.info().clientID != null && p.info().clientID == this.playerInfo.clientID)
if (existing) {
existing.tiles().forEach(t => existing.relinquish(t))
getSpawnCells(this.mg, this.cell).forEach(c => {
existing.conquer(this.mg.tile(c))
})
return
}
const player = this.mg.addPlayer(this.playerInfo, this.mg.config().player().startTroops(this.playerInfo))
getSpawnCells(this.mg, this.cell).forEach(c => {
player.conquer(this.mg.tile(c))
@@ -39,4 +53,9 @@ export class SpawnExecution implements Execution {
isActive(): boolean {
return this.active
}
activeDuringSpawnPhase(): boolean {
return true
}
}