mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:00:44 +00:00
can change spawn in beginning of game
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user