mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 11:13:27 +00:00
First Commit
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
export interface GameEvent { }
|
||||
|
||||
export interface EventConstructor<T extends GameEvent = GameEvent> {
|
||||
new(...args: any[]): T;
|
||||
}
|
||||
|
||||
export class EventBus {
|
||||
private listeners: Map<EventConstructor, Array<(event: GameEvent) => void>> = new Map();
|
||||
|
||||
emit<T extends GameEvent>(event: T): void {
|
||||
const eventConstructor = event.constructor as EventConstructor<T>;
|
||||
const callbacks = this.listeners.get(eventConstructor);
|
||||
if (callbacks) {
|
||||
for (const callback of callbacks) {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
on<T extends GameEvent>(
|
||||
eventType: EventConstructor<T>,
|
||||
callback: (event: T) => void
|
||||
): void {
|
||||
if (!this.listeners.has(eventType)) {
|
||||
this.listeners.set(eventType, []);
|
||||
}
|
||||
const callbacks = this.listeners.get(eventType)!;
|
||||
callbacks.push(callback as (event: GameEvent) => void);
|
||||
}
|
||||
|
||||
off<T extends GameEvent>(eventType: EventConstructor<T>, callback: (event: T) => void): void {
|
||||
const callbacks = this.listeners.get(eventType);
|
||||
if (callbacks) {
|
||||
const index = callbacks.indexOf(callback as (event: GameEvent) => void);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import {GameEvent} from "./EventBus"
|
||||
|
||||
export type ClientID = string
|
||||
|
||||
export type PlayerID = number // TODO: make string?
|
||||
|
||||
export type GameID = string
|
||||
|
||||
export type LobbyID = string
|
||||
|
||||
export class Cell {
|
||||
constructor(
|
||||
public readonly x,
|
||||
public readonly y
|
||||
) { }
|
||||
|
||||
toString(): string {return `Cell[${this.x},${this.y}]`}
|
||||
}
|
||||
|
||||
export interface ExecutionView {
|
||||
isActive(): boolean
|
||||
owner(): Player
|
||||
}
|
||||
|
||||
export interface Execution extends ExecutionView {
|
||||
init(mg: MutableGame, ticks: number)
|
||||
tick(ticks: number)
|
||||
owner(): MutablePlayer
|
||||
}
|
||||
|
||||
export class PlayerInfo {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly isBot: boolean
|
||||
) { }
|
||||
}
|
||||
|
||||
// TODO: make terrain api better.
|
||||
export class Terrain {
|
||||
constructor(
|
||||
public readonly expansionCost: number,
|
||||
public readonly expansionTime: number,
|
||||
) { }
|
||||
}
|
||||
|
||||
export type TerrainType = typeof TerrainTypes[keyof typeof TerrainTypes];
|
||||
|
||||
export const TerrainTypes = {
|
||||
Land: new Terrain(1, 1),
|
||||
Water: new Terrain(0, 0)
|
||||
}
|
||||
|
||||
export interface TerrainMap {
|
||||
terrain(cell: Cell): Terrain
|
||||
width(): number
|
||||
height(): number
|
||||
}
|
||||
|
||||
export interface Tile {
|
||||
owner(): Player | TerraNullius
|
||||
hasOwner(): boolean
|
||||
isBorder(): boolean
|
||||
isInterior(): boolean
|
||||
cell(): Cell
|
||||
terrain(): Terrain
|
||||
game(): Game
|
||||
neighbors(): Tile[]
|
||||
onShore(): boolean
|
||||
}
|
||||
|
||||
export interface Boat {
|
||||
troops(): number
|
||||
cell(): Cell
|
||||
owner(): Player
|
||||
target(): Player | TerraNullius
|
||||
}
|
||||
|
||||
export interface MutableBoat extends Boat {
|
||||
move(cell: Cell): void
|
||||
owner(): MutablePlayer
|
||||
target(): MutablePlayer | TerraNullius
|
||||
setTroops(troops: number): void
|
||||
}
|
||||
|
||||
export interface TerraNullius {
|
||||
ownsTile(cell: Cell): boolean
|
||||
isPlayer(): false
|
||||
}
|
||||
|
||||
export interface Player {
|
||||
info(): PlayerInfo
|
||||
id(): PlayerID
|
||||
troops(): number
|
||||
boats(): Boat[]
|
||||
ownsTile(cell: Cell): boolean
|
||||
isAlive(): boolean
|
||||
executions(): ExecutionView[]
|
||||
borderTiles(): ReadonlySet<Tile>
|
||||
borderTilesWith(other: Player | TerraNullius): ReadonlySet<Tile>
|
||||
isPlayer(): this is Player
|
||||
neighbors(): (Player | TerraNullius)[]
|
||||
numTilesOwned(): number
|
||||
sharesBorderWith(other: Player | TerraNullius): boolean
|
||||
}
|
||||
|
||||
export interface MutablePlayer extends Player {
|
||||
setTroops(troops: number): void
|
||||
addTroops(troops: number): void
|
||||
removeTroops(troops: number): void
|
||||
conquer(cell: Cell): void
|
||||
executions(): Execution[]
|
||||
neighbors(): (MutablePlayer | TerraNullius)[]
|
||||
boats(): MutableBoat[]
|
||||
addBoat(troops: number, cell: Cell, target: Player | TerraNullius): MutableBoat
|
||||
}
|
||||
|
||||
export interface Game {
|
||||
// Throws exception is player not found
|
||||
player(id: PlayerID): Player
|
||||
players(): Player[]
|
||||
tile(cell: Cell): Tile
|
||||
isOnMap(cell: Cell): boolean
|
||||
neighbors(cell: Cell): Cell[]
|
||||
width(): number
|
||||
height(): number
|
||||
forEachTile(fn: (tile: Tile) => void): void
|
||||
executions(): ExecutionView[]
|
||||
terraNullius(): TerraNullius
|
||||
tick()
|
||||
addExecution(...exec: Execution[])
|
||||
}
|
||||
|
||||
export interface MutableGame extends Game {
|
||||
player(id: PlayerID): MutablePlayer
|
||||
players(): MutablePlayer[]
|
||||
addPlayer(playerInfo: PlayerInfo): MutablePlayer
|
||||
executions(): Execution[]
|
||||
removeInactiveExecutions(): void
|
||||
removeExecution(exec: Execution)
|
||||
}
|
||||
|
||||
|
||||
export class TileEvent implements GameEvent {
|
||||
constructor(public readonly tile: Tile) { }
|
||||
}
|
||||
|
||||
export class PlayerEvent implements GameEvent {
|
||||
constructor(public readonly player: Player) { }
|
||||
}
|
||||
|
||||
export class BoatEvent implements GameEvent {
|
||||
constructor(public readonly boat: Boat) { }
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
import {EventBus} from "./EventBus";
|
||||
import {Cell, Execution, MutableGame, Game, MutablePlayer, PlayerEvent, PlayerID, PlayerInfo, Player, TerrainMap, TerrainType, TerrainTypes, TerraNullius, Tile, TileEvent, Boat, MutableBoat, BoatEvent} from "./Game";
|
||||
|
||||
export function createGame(terrainMap: TerrainMap, eventBus: EventBus): Game {
|
||||
return new GameImpl(terrainMap, eventBus)
|
||||
}
|
||||
|
||||
type CellString = string
|
||||
|
||||
class TileImpl implements Tile {
|
||||
|
||||
constructor(
|
||||
private readonly gs: GameImpl,
|
||||
public _owner: PlayerImpl | TerraNulliusImpl,
|
||||
private readonly _cell: Cell,
|
||||
private readonly _terrain: TerrainType
|
||||
) { }
|
||||
|
||||
onShore(): boolean {
|
||||
return this.neighbors()
|
||||
.filter(t => t.terrain() == TerrainTypes.Water)
|
||||
.length > 0
|
||||
}
|
||||
|
||||
hasOwner(): boolean {return this._owner != this.gs._terraNullius}
|
||||
owner(): MutablePlayer | TerraNullius {return this._owner}
|
||||
isBorder(): boolean {return this.gs.isBorder(this)}
|
||||
isInterior(): boolean {return this.hasOwner() && !this.isBorder()}
|
||||
cell(): Cell {return this._cell}
|
||||
terrain(): TerrainType {return this._terrain}
|
||||
|
||||
neighbors(): Tile[] {
|
||||
return this.gs.neighbors(this._cell).map(c => this.gs.tile(c))
|
||||
}
|
||||
|
||||
game(): Game {return this.gs}
|
||||
}
|
||||
|
||||
export class BoatImpl implements MutableBoat {
|
||||
|
||||
constructor(
|
||||
private g: GameImpl,
|
||||
private _cell: Cell,
|
||||
private _troops: number,
|
||||
private _owner: PlayerImpl,
|
||||
private _target: PlayerImpl | TerraNulliusImpl
|
||||
) { }
|
||||
|
||||
move(cell: Cell): void {
|
||||
this._cell = cell
|
||||
this.g.fireBoatUpdateEvent(this)
|
||||
}
|
||||
setTroops(troops: number): void {
|
||||
this._troops = troops
|
||||
}
|
||||
troops(): number {
|
||||
return this._troops
|
||||
}
|
||||
cell(): Cell {
|
||||
return this._cell
|
||||
}
|
||||
owner(): PlayerImpl {
|
||||
return this._owner
|
||||
}
|
||||
target(): PlayerImpl | TerraNullius {
|
||||
return this._target
|
||||
}
|
||||
}
|
||||
|
||||
export class PlayerImpl implements MutablePlayer {
|
||||
|
||||
public _boats: BoatImpl[] = []
|
||||
|
||||
public _borderTiles: Map<CellString, Tile> = new Map()
|
||||
_borderWith: Map<Player | TerraNullius, Set<Tile>> = new Map()
|
||||
public tiles: Map<CellString, Tile> = new Map<CellString, Tile>()
|
||||
|
||||
constructor(private gs: GameImpl, public readonly _id: PlayerID, public readonly playerInfo: PlayerInfo, private _troops) { }
|
||||
|
||||
addBoat(troops: number, cell: Cell, target: Player | TerraNullius): BoatImpl {
|
||||
const b = new BoatImpl(this.gs, cell, troops, this, target as PlayerImpl | TerraNulliusImpl)
|
||||
this._boats.push(b)
|
||||
this.gs.fireBoatUpdateEvent(b)
|
||||
return b
|
||||
}
|
||||
boats(): BoatImpl[] {
|
||||
return this._boats
|
||||
}
|
||||
sharesBorderWith(other: Player | TerraNullius): boolean {
|
||||
if (!this._borderWith.has(other)) {
|
||||
return false
|
||||
}
|
||||
return this._borderWith.get(other).size > 0
|
||||
}
|
||||
numTilesOwned(): number {
|
||||
return this.tiles.size
|
||||
}
|
||||
|
||||
borderTiles(): ReadonlySet<Tile> {
|
||||
return new Set(this._borderTiles.values())
|
||||
}
|
||||
|
||||
neighbors(): (MutablePlayer | TerraNullius)[] {
|
||||
const ns: (MutablePlayer | TerraNullius)[] = []
|
||||
for (const [player, tiles] of this._borderWith) {
|
||||
if (tiles.size > 0) {
|
||||
ns.push(player as MutablePlayer)
|
||||
}
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
addTroops(troops: number): void {
|
||||
this._troops += troops
|
||||
}
|
||||
removeTroops(troops: number): void {
|
||||
this._troops -= troops
|
||||
}
|
||||
|
||||
isPlayer(): this is MutablePlayer {return true as const}
|
||||
ownsTile(cell: Cell): boolean {return this.tiles.has(cell.toString())}
|
||||
setTroops(troops: number) {this._troops = troops}
|
||||
conquer(cell: Cell) {this.gs.conquer(this, cell)}
|
||||
info(): PlayerInfo {return this.playerInfo}
|
||||
id(): PlayerID {return this._id}
|
||||
troops(): number {return this._troops}
|
||||
isAlive(): boolean {return this.tiles.size > 0}
|
||||
gameState(): MutableGame {return this.gs}
|
||||
executions(): Execution[] {
|
||||
return this.gs.executions().filter(exec => exec.owner().id() == this.id())
|
||||
}
|
||||
|
||||
borderTilesWith(other: Player | TerraNullius): ReadonlySet<Tile> {
|
||||
return this._borderWith.get(other) || new Set();
|
||||
}
|
||||
|
||||
updateBorderWithTile(tile: Tile, oldOwner: Player | TerraNullius, newOwner: Player | TerraNullius) {
|
||||
if (!this._borderWith.has(oldOwner)) {
|
||||
this._borderWith.set(oldOwner, new Set())
|
||||
}
|
||||
if (!this._borderWith.has(newOwner)) {
|
||||
this._borderWith.set(newOwner, new Set())
|
||||
}
|
||||
|
||||
// Delete old neighbors
|
||||
if (this.gs.tileNeighbors(tile).filter(t => t.owner() == newOwner).length == 0) {
|
||||
this._borderWith.get(oldOwner).delete(tile)
|
||||
}
|
||||
}
|
||||
|
||||
addCalcBorderWithTile(tile: Tile) {
|
||||
this.gs.neighbors(tile.cell()).map(c => this.gs.tile(c)).forEach(t => {
|
||||
this.insertBorderWithTile(tile, t.owner())
|
||||
})
|
||||
}
|
||||
|
||||
removeCalcBorderWithTile(tile: Tile, oldNeighbor: Player | TerraNullius) {
|
||||
const length = this.gs.neighbors(tile.cell()).map(c => this.gs.tile(c)).filter(t => t.owner() == oldNeighbor).length
|
||||
if (length == 0) {
|
||||
this.deleteBorderWithTile(tile, oldNeighbor)
|
||||
}
|
||||
}
|
||||
|
||||
insertBorderWithTile(tile: Tile, player: Player | TerraNullius) {
|
||||
if (!this._borderWith.has(player)) {
|
||||
this._borderWith.set(player, new Set())
|
||||
}
|
||||
if (player != this) {
|
||||
this._borderWith.get(player).add(tile)
|
||||
}
|
||||
}
|
||||
|
||||
deleteBorderWithTile(tile: Tile, player: Player | TerraNullius) {
|
||||
if (!this._borderWith.has(player)) {
|
||||
this._borderWith.set(player, new Set())
|
||||
}
|
||||
this._borderWith.get(player).delete(tile)
|
||||
}
|
||||
}
|
||||
|
||||
class TerraNulliusImpl implements TerraNullius {
|
||||
_borderWith: Map<Player | TerraNullius, Set<Tile>> = new Map()
|
||||
public tiles: Map<Cell, Tile> = new Map<Cell, Tile>()
|
||||
|
||||
constructor(private gs: MutableGame) { }
|
||||
|
||||
id(): PlayerID {
|
||||
return 0
|
||||
}
|
||||
ownsTile(cell: Cell): boolean {
|
||||
return this.tiles.has(cell)
|
||||
}
|
||||
isPlayer(): false {return false as const}
|
||||
|
||||
}
|
||||
|
||||
export class TerrainMapImpl implements TerrainMap {
|
||||
|
||||
constructor(public readonly tiles: TerrainType[][]) { }
|
||||
|
||||
terrain(cell: Cell): TerrainType {
|
||||
return this.tiles[cell.x][cell.y]
|
||||
}
|
||||
|
||||
width(): number {
|
||||
return this.tiles.length
|
||||
}
|
||||
|
||||
height(): number {
|
||||
return this.tiles[0].length
|
||||
}
|
||||
}
|
||||
|
||||
export class GameImpl implements MutableGame {
|
||||
private ticks = 0
|
||||
|
||||
private unInitExecs: Execution[] = []
|
||||
|
||||
idCounter: PlayerID = 1; // Zero reserved for TerraNullius
|
||||
map: TileImpl[][]
|
||||
_players: Map<PlayerID, PlayerImpl> = new Map<PlayerID, PlayerImpl>
|
||||
private execs: Execution[] = []
|
||||
private _width: number
|
||||
private _height: number
|
||||
_terraNullius: TerraNulliusImpl
|
||||
|
||||
constructor(terrainMap: TerrainMap, private eventBus: EventBus) {
|
||||
this._terraNullius = new TerraNulliusImpl(this)
|
||||
this._width = terrainMap.width();
|
||||
this._height = terrainMap.height();
|
||||
this.map = new Array(this._width);
|
||||
for (let x = 0; x < this._width; x++) {
|
||||
this.map[x] = new Array(this._height);
|
||||
for (let y = 0; y < this._height; y++) {
|
||||
let cell = new Cell(x, y);
|
||||
this.map[x][y] = new TileImpl(this, this._terraNullius, cell, terrainMap.terrain(cell));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.executions().forEach(e => e.tick(this.ticks))
|
||||
this.unInitExecs.forEach(e => e.init(this, this.ticks))
|
||||
|
||||
this.removeInactiveExecutions()
|
||||
|
||||
this.execs.push(...this.unInitExecs)
|
||||
this.unInitExecs = []
|
||||
this.ticks++
|
||||
}
|
||||
|
||||
terraNullius(): TerraNullius {
|
||||
return this._terraNullius
|
||||
}
|
||||
|
||||
removeInactiveExecutions(): void {
|
||||
this.execs = this.execs.filter(e => e.isActive())
|
||||
}
|
||||
|
||||
players(): MutablePlayer[] {
|
||||
return Array.from(this._players.values()).filter(p => p.isAlive())
|
||||
}
|
||||
|
||||
executions(): Execution[] {
|
||||
return this.execs
|
||||
}
|
||||
|
||||
addExecution(...exec: Execution[]) {
|
||||
this.unInitExecs.push(...exec)
|
||||
}
|
||||
|
||||
removeExecution(exec: Execution) {
|
||||
this.execs.filter(execution => execution !== exec)
|
||||
}
|
||||
|
||||
width(): number {
|
||||
return this._width
|
||||
}
|
||||
|
||||
height(): number {
|
||||
return this._height
|
||||
}
|
||||
|
||||
forEachTile(fn: (tile: Tile) => void): void {
|
||||
for (let x = 0; x < this._width; x++) {
|
||||
for (let y = 0; y < this._height; y++) {
|
||||
fn(this.tile(new Cell(x, y)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
playerView(id: PlayerID): MutablePlayer {
|
||||
return this.player(id)
|
||||
}
|
||||
|
||||
addPlayer(playerInfo: PlayerInfo): MutablePlayer {
|
||||
let id = this.idCounter
|
||||
this.idCounter++
|
||||
let player = new PlayerImpl(this, id, playerInfo, 10000)
|
||||
this._players.set(id, player)
|
||||
this.eventBus.emit(new PlayerEvent(player))
|
||||
return player
|
||||
}
|
||||
|
||||
player(id: PlayerID | null): MutablePlayer {
|
||||
if (!this._players.has(id)) {
|
||||
throw new Error(`Player with id ${id} not found`)
|
||||
}
|
||||
return this._players.get(id)
|
||||
}
|
||||
|
||||
tile(cell: Cell): Tile {
|
||||
this.assertIsOnMap(cell)
|
||||
return this.map[cell.x][cell.y]
|
||||
}
|
||||
|
||||
isOnMap(cell: Cell): boolean {
|
||||
return cell.x >= 0
|
||||
&& cell.x < this._width
|
||||
&& cell.y >= 0
|
||||
&& cell.y < this._height
|
||||
}
|
||||
|
||||
neighbors(cell: Cell): Cell[] {
|
||||
this.assertIsOnMap(cell)
|
||||
return [
|
||||
new Cell(cell.x + 1, cell.y),
|
||||
new Cell(cell.x - 1, cell.y),
|
||||
new Cell(cell.x, cell.y + 1),
|
||||
new Cell(cell.x, cell.y - 1)
|
||||
].filter(c => this.isOnMap(c))
|
||||
}
|
||||
|
||||
tileNeighbors(tile: Tile): Tile[] {
|
||||
return this.neighbors(tile.cell()).map(c => this.tile(c))
|
||||
}
|
||||
|
||||
private assertIsOnMap(cell: Cell) {
|
||||
if (!this.isOnMap(cell)) {
|
||||
throw new Error(`cell ${cell.toString()} is not on map`)
|
||||
}
|
||||
}
|
||||
|
||||
conquer(owner: PlayerImpl, cell: Cell): void {
|
||||
if (owner.ownsTile(cell)) {
|
||||
throw new Error(`Player ${owner} already owns cell ${cell.toString()}`)
|
||||
}
|
||||
if (!owner.isPlayer()) {
|
||||
throw new Error("Must be a player")
|
||||
}
|
||||
let tile = this.tile(cell) as TileImpl
|
||||
let previousOwner = tile._owner
|
||||
if (previousOwner.isPlayer()) {
|
||||
previousOwner.tiles.delete(cell.toString())
|
||||
previousOwner._borderTiles.delete(cell.toString())
|
||||
}
|
||||
tile._owner = owner
|
||||
owner.tiles.set(cell.toString(), tile)
|
||||
this.updateBorders(cell)
|
||||
this.updateBordersWith(tile, previousOwner)
|
||||
this.eventBus.emit(new TileEvent(tile))
|
||||
}
|
||||
|
||||
private updateBorders(cell: Cell) {
|
||||
const cells: Cell[] = []
|
||||
cells.push(cell)
|
||||
this.neighbors(cell).forEach(c => cells.push(c))
|
||||
cells.map(c => this.tile(c)).filter(c => c.hasOwner()).forEach(t => {
|
||||
if (this.isBorder(t)) {
|
||||
(t.owner() as PlayerImpl)._borderTiles.set(t.cell().toString(), t)
|
||||
} else {
|
||||
(t.owner() as PlayerImpl)._borderTiles.delete(t.cell().toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private updateBordersWith(tile: TileImpl, previousOwner: PlayerImpl | TerraNulliusImpl) {
|
||||
const newOwner = tile._owner
|
||||
const neighbors = this.neighbors(tile.cell()).map(c => this.tile(c))
|
||||
|
||||
if (newOwner.isPlayer()) {
|
||||
newOwner.addCalcBorderWithTile(tile)
|
||||
}
|
||||
|
||||
neighbors.map(t => (t as TileImpl)).forEach(t => {
|
||||
const p = t._owner
|
||||
if (p.isPlayer()) {
|
||||
p.addCalcBorderWithTile(t)
|
||||
p.removeCalcBorderWithTile(t, previousOwner)
|
||||
}
|
||||
if (previousOwner.isPlayer()) {
|
||||
previousOwner.deleteBorderWithTile(tile, p)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
isBorder(tile: Tile): boolean {
|
||||
this.assertIsOnMap(tile.cell())
|
||||
if (!tile.hasOwner()) {
|
||||
return false
|
||||
}
|
||||
for (const neighbor of this.neighbors(tile.cell())) {
|
||||
let bordersEnemy = this.tile(neighbor).owner() != tile.owner()
|
||||
if (bordersEnemy) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public fireBoatUpdateEvent(boat: Boat) {
|
||||
this.eventBus.emit(new BoatEvent(boat))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export class PseudoRandom {
|
||||
private m: number = 0x80000000; // 2**31
|
||||
private a: number = 1103515245;
|
||||
private c: number = 12345;
|
||||
private state: number;
|
||||
|
||||
constructor(seed: number) {
|
||||
this.state = seed % this.m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the next pseudorandom number.
|
||||
* @returns A number between 0 (inclusive) and 1 (exclusive).
|
||||
*/
|
||||
next(): number {
|
||||
this.state = (this.a * this.state + this.c) % this.m;
|
||||
return this.state / this.m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random integer between min (inclusive) and max (exclusive).
|
||||
*/
|
||||
nextInt(min: number, max: number): number {
|
||||
return Math.floor(this.next() * (max - min) + min);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random float between min (inclusive) and max (exclusive).
|
||||
*/
|
||||
nextFloat(min: number, max: number): number {
|
||||
return this.next() * (max - min) + min;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import {z} from 'zod';
|
||||
|
||||
export type Intent = SpawnIntent | AttackIntent | BoatAttackIntent
|
||||
|
||||
export type AttackIntent = z.infer<typeof AttackIntentSchema>
|
||||
export type SpawnIntent = z.infer<typeof SpawnIntentSchema>
|
||||
export type BoatAttackIntent = z.infer<typeof BoatAttackIntentSchema>
|
||||
|
||||
export type Turn = z.infer<typeof TurnSchema>
|
||||
|
||||
export type ClientMessage = ClientIntentMessage | ClientJoinMessage
|
||||
export type ServerMessage = ServerSyncMessage | ServerStartGameMessage
|
||||
|
||||
export type ServerSyncMessage = z.infer<typeof ServerTurnMessageSchema>
|
||||
export type ServerStartGameMessage = z.infer<typeof ServerStartGameMessageSchema>
|
||||
|
||||
|
||||
export type ClientIntentMessage = z.infer<typeof ClientIntentMessageSchema>
|
||||
export type ClientJoinMessage = z.infer<typeof ClientJoinMessageSchema>
|
||||
|
||||
|
||||
|
||||
// Zod schemas
|
||||
const BaseIntentSchema = z.object({
|
||||
type: z.enum(['attack', 'spawn', 'boat']),
|
||||
});
|
||||
|
||||
export const AttackIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('attack'),
|
||||
attackerID: z.number(),
|
||||
targetID: z.number().nullable(),
|
||||
troops: z.number(),
|
||||
targetX: z.number(),
|
||||
targetY: z.number()
|
||||
});
|
||||
|
||||
|
||||
export const SpawnIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('spawn'),
|
||||
name: z.string(),
|
||||
isBot: z.boolean(),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
})
|
||||
|
||||
export const BoatAttackIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('boat'),
|
||||
attackerID: z.number(),
|
||||
targetID: z.number().nullable(),
|
||||
troops: z.number(),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
})
|
||||
|
||||
const IntentSchema = z.union([AttackIntentSchema, SpawnIntentSchema, BoatAttackIntentSchema]);
|
||||
|
||||
const TurnSchema = z.object({
|
||||
turnNumber: z.number(),
|
||||
intents: z.array(IntentSchema)
|
||||
})
|
||||
|
||||
// Server
|
||||
|
||||
const ServerBaseMessageSchema = z.object({
|
||||
type: z.string()
|
||||
})
|
||||
|
||||
export const ServerTurnMessageSchema = ServerBaseMessageSchema.extend({
|
||||
type: z.literal('turn'),
|
||||
turn: TurnSchema,
|
||||
})
|
||||
|
||||
export const ServerStartGameMessageSchema = ServerBaseMessageSchema.extend({
|
||||
type: z.literal('start'),
|
||||
})
|
||||
|
||||
|
||||
export const ServerMessageSchema = z.union([ServerTurnMessageSchema, ServerStartGameMessageSchema]);
|
||||
|
||||
|
||||
// Client
|
||||
|
||||
const ClientBaseMessageSchema = z.object({
|
||||
type: z.string()
|
||||
})
|
||||
|
||||
export const ClientIntentMessageSchema = ClientBaseMessageSchema.extend({
|
||||
type: z.literal('intent'),
|
||||
clientID: z.string(),
|
||||
//gameID: z.string(),
|
||||
intent: IntentSchema
|
||||
})
|
||||
|
||||
export const ClientJoinMessageSchema = ClientBaseMessageSchema.extend({
|
||||
type: z.literal('join'),
|
||||
clientID: z.string(),
|
||||
lobbyID: z.string()
|
||||
})
|
||||
|
||||
export const ClientMessageSchema = z.union([ClientIntentMessageSchema, ClientJoinMessageSchema]);
|
||||
@@ -0,0 +1,84 @@
|
||||
import {PlayerID, TerrainType, TerrainTypes} from "./Game";
|
||||
import {Colord, colord} from "colord";
|
||||
|
||||
export interface Settings {
|
||||
theme(): Theme;
|
||||
turnIntervalMs(): number
|
||||
tickIntervalMs(): number
|
||||
ticksPerTurn(): number
|
||||
lobbyCreationRate(): number
|
||||
lobbyLifetime(): number
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
playerInfoColor(id: PlayerID): Colord;
|
||||
territoryColor(id: PlayerID): Colord;
|
||||
borderColor(id: PlayerID): Colord;
|
||||
terrainColor(tile: TerrainType): Colord;
|
||||
backgroundColor(): Colord;
|
||||
font(): string;
|
||||
shaderArgs(): {name: string; args: {[key: string]: any}}[];
|
||||
}
|
||||
|
||||
export const defaultSettings = new class implements Settings {
|
||||
ticksPerTurn(): number {
|
||||
return 1
|
||||
}
|
||||
turnIntervalMs(): number {
|
||||
return 1000 / 10
|
||||
}
|
||||
lobbyCreationRate(): number {
|
||||
return 5 * 1000
|
||||
}
|
||||
lobbyLifetime(): number {
|
||||
return 2 * 1000
|
||||
}
|
||||
theme(): Theme {return pastelTheme;}
|
||||
|
||||
tickIntervalMs(): number {
|
||||
return 1000 / 20; // 50ms
|
||||
}
|
||||
}
|
||||
|
||||
const pastelTheme = new class implements Theme {
|
||||
private background = colord({r: 100, g: 100, b: 100});
|
||||
private land = colord({r: 244, g: 243, b: 198});
|
||||
private water = colord({r: 160, g: 203, b: 231});
|
||||
private territory = colord({r: 173, g: 216, b: 230});
|
||||
|
||||
playerInfoColor(id: PlayerID): Colord {
|
||||
return colord({r: 0, g: 0, b: 0})
|
||||
}
|
||||
|
||||
territoryColor(id: PlayerID): Colord {
|
||||
return colord({r: (id * 10) % 250, g: (id * 100) % 250, b: (id) % 250});
|
||||
}
|
||||
|
||||
borderColor(id: PlayerID): Colord {
|
||||
const tc = this.territoryColor(id).rgba;
|
||||
return colord({
|
||||
r: Math.min(tc.r + 20, 255),
|
||||
g: Math.min(tc.g + 20, 255),
|
||||
b: Math.min(tc.b + 20, 255)
|
||||
})
|
||||
}
|
||||
|
||||
terrainColor(tile: TerrainType): Colord {
|
||||
if (tile == TerrainTypes.Land) {
|
||||
return this.land;
|
||||
}
|
||||
return this.water;
|
||||
}
|
||||
|
||||
backgroundColor(): Colord {
|
||||
return this.background;
|
||||
}
|
||||
|
||||
font(): string {
|
||||
return "Overpass";
|
||||
}
|
||||
|
||||
shaderArgs(): {name: string; args: {[key: string]: any}}[] {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {Jimp as JimpType, JimpConstructors} from '@jimp/core';
|
||||
import 'jimp';
|
||||
import {TerrainMap, TerrainType, TerrainTypes} from './Game';
|
||||
import {TerrainMapImpl} from './GameImpl';
|
||||
|
||||
declare const Jimp: JimpType & JimpConstructors;
|
||||
|
||||
export async function loadTerrainMap(): Promise<TerrainMap> {
|
||||
const imageModule = await import(`../../resources/maps/World.png`);
|
||||
const imageUrl = imageModule.default;
|
||||
const image = await Jimp.read(imageUrl)
|
||||
const {width, height} = image.bitmap;
|
||||
|
||||
const terrain: TerrainType[][] = Array(width).fill(null).map(() => Array(height).fill(TerrainTypes.Water));
|
||||
|
||||
image.scan(0, 0, width, height, function (x: number, y: number, idx: number) {
|
||||
const red = this.bitmap.data[idx + 0];
|
||||
|
||||
if (red > 100) {
|
||||
terrain[x][y] = TerrainTypes.Land;
|
||||
}
|
||||
})
|
||||
|
||||
return new TerrainMapImpl(terrain);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {EventBus, GameEvent} from "./EventBus";
|
||||
import {Settings} from "./Settings";
|
||||
|
||||
export class TickEvent implements GameEvent {
|
||||
constructor(public readonly tickCount: number) { }
|
||||
}
|
||||
|
||||
export class Ticker {
|
||||
private ticker: NodeJS.Timeout;
|
||||
private tickCount: number;
|
||||
|
||||
constructor(private tickInterval: number, private eventBus: EventBus) {
|
||||
|
||||
}
|
||||
|
||||
start() {
|
||||
this.tickCount = 0;
|
||||
this.ticker = setInterval(() => this.tick(), this.tickInterval);
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearInterval(this.ticker);
|
||||
}
|
||||
|
||||
private tick() {
|
||||
this.eventBus.emit(new TickEvent(this.tickCount))
|
||||
this.tickCount++;
|
||||
}
|
||||
|
||||
getTickCount(): number {
|
||||
return this.tickCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import {Cell} from "./Game";
|
||||
|
||||
export function generateUniqueID(): string {
|
||||
const array = new Uint8Array(16);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
export function manhattanDist(c1: Cell, c2: Cell): number {
|
||||
return Math.abs(c1.x - c2.x) + Math.abs(c1.y - c2.y);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import PriorityQueue from "priority-queue-typescript";
|
||||
import {Cell, Execution, MutableGame, MutablePlayer, PlayerID, Player, TerrainTypes, TerraNullius, Tile} from "../Game";
|
||||
import {PseudoRandom} from "../PseudoRandom";
|
||||
import {manhattanDist} from "../Util";
|
||||
|
||||
export class AttackExecution implements Execution {
|
||||
private active: boolean = true;
|
||||
private toConquer: PriorityQueue<TileContainer> = new PriorityQueue<TileContainer>(11, (a: TileContainer, b: TileContainer) => a.priority - b.priority);
|
||||
private random = new PseudoRandom(123)
|
||||
|
||||
private _owner: MutablePlayer
|
||||
private target: MutablePlayer | TerraNullius
|
||||
|
||||
constructor(
|
||||
private troops: number,
|
||||
private _ownerID: PlayerID,
|
||||
private targetID: PlayerID | null,
|
||||
private targetCell: Cell | null
|
||||
) { }
|
||||
|
||||
init(gs: MutableGame, ticks: number) {
|
||||
this._owner = gs.player(this._ownerID)
|
||||
this.target = this.targetID == null ? gs.terraNullius() : gs.player(this.targetID)
|
||||
this.troops = Math.min(this._owner.troops(), this.troops)
|
||||
this._owner.setTroops(this._owner.troops() - this.troops)
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (!this.active) {
|
||||
return
|
||||
}
|
||||
|
||||
let numTilesPerTick = this._owner.borderTilesWith(this.target).size / 2
|
||||
while (numTilesPerTick > 0) {
|
||||
if (this.troops < 1) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
|
||||
if (this.toConquer.size() == 0) {
|
||||
this.calculateToConquer()
|
||||
}
|
||||
if (this.toConquer.size() == 0) {
|
||||
this.active = false
|
||||
this._owner.addTroops(this.troops)
|
||||
return
|
||||
}
|
||||
|
||||
const tileToConquer: Tile = this.toConquer.poll().tile
|
||||
const onBorder = tileToConquer.neighbors().filter(t => t.owner() == this._owner).length > 0
|
||||
if (tileToConquer.owner() != this.target || !onBorder) {
|
||||
continue
|
||||
}
|
||||
this._owner.conquer(tileToConquer.cell())
|
||||
this.troops -= 1
|
||||
numTilesPerTick -= 1
|
||||
}
|
||||
}
|
||||
|
||||
private calculateToConquer() {
|
||||
const border = this.owner().borderTilesWith(this.target)
|
||||
const enemyBorder: Set<Tile> = new Set()
|
||||
for (const b of border) {
|
||||
b.neighbors()
|
||||
.filter(t => t.terrain() == TerrainTypes.Land)
|
||||
.filter(t => t.owner() == this.target)
|
||||
.forEach(t => enemyBorder.add(t))
|
||||
}
|
||||
|
||||
// let closestTile: Tile;
|
||||
// let closestDist: number = Number.POSITIVE_INFINITY;
|
||||
// for (const enemyTile of enemyBorder) {
|
||||
// const dist = manhattanDist(enemyTile.cell(), this.targetCell)
|
||||
// if (dist < closestDist) {
|
||||
// closestTile = enemyTile
|
||||
// }
|
||||
// }
|
||||
|
||||
// tileByDist.forEach(t => console.log(`tile dist: ${manhattanDist(t.cell(), closestTile.cell())}`))
|
||||
let tileByDist = []
|
||||
if (this.targetCell == null) {
|
||||
tileByDist = Array.from(enemyBorder).slice().sort((a, b) => this.random.next() - .5)
|
||||
} else {
|
||||
tileByDist = Array.from(enemyBorder).slice().sort((a, b) => manhattanDist(a.cell(), this.targetCell) - manhattanDist(b.cell(), this.targetCell))
|
||||
}
|
||||
for (let i = 0; i < Math.min(enemyBorder.size / 2, tileByDist.length); i++) {
|
||||
const enemyTile = tileByDist[i]
|
||||
const numOwnedByMe = enemyTile.neighbors()
|
||||
.filter(t => t.terrain() == TerrainTypes.Land)
|
||||
.filter(t => t.owner() == this._owner)
|
||||
.length
|
||||
// this.toConquer.add(new TileContainer(enemyTile, numOwnedByMe + (this.random.next() % 5) + (-5 * i / tileByDist.length)))
|
||||
const r = this.random.next() % 4
|
||||
this.toConquer.add(new TileContainer(enemyTile, r + numOwnedByMe * 1000))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
owner(): MutablePlayer {
|
||||
return this._owner
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class TileContainer {
|
||||
constructor(public readonly tile: Tile, public readonly priority: number) { }
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import PriorityQueue from "priority-queue-typescript";
|
||||
import {Boat, Cell, Execution, MutableBoat, MutableGame, MutablePlayer, Player, PlayerID, Tile} from "../Game";
|
||||
import {manhattanDist} from "../Util";
|
||||
import {AttackExecution} from "./AttackExecution";
|
||||
|
||||
export class BoatAttackExecution implements Execution {
|
||||
|
||||
private lastMove: number
|
||||
|
||||
// TODO: make this configurable
|
||||
private ticksPerMove = 1
|
||||
|
||||
private active = true
|
||||
|
||||
private mg: MutableGame
|
||||
private attacker: MutablePlayer
|
||||
private target: MutablePlayer
|
||||
|
||||
// TODO make private
|
||||
public path: Tile[]
|
||||
private src: Tile
|
||||
private dst: Tile
|
||||
|
||||
private currTileIndex: number = 0
|
||||
|
||||
private boat: MutableBoat
|
||||
|
||||
constructor(
|
||||
private attackerID: PlayerID,
|
||||
private targetID: PlayerID | null,
|
||||
private cell: Cell,
|
||||
private troops: number
|
||||
) { }
|
||||
|
||||
init(mg: MutableGame, ticks: number) {
|
||||
if (this.targetID == null) {
|
||||
throw new Error("attacking terranullius not supported")
|
||||
}
|
||||
this.lastMove = ticks
|
||||
|
||||
this.mg = mg
|
||||
this.attacker = mg.player(this.attackerID)
|
||||
this.target = mg.player(this.targetID)
|
||||
|
||||
this.troops = Math.min(this.troops, this.attacker.troops())
|
||||
this.attacker.removeTroops(this.troops)
|
||||
|
||||
this.src = this.closestShoreTileToTarget(this.attacker, this.cell)
|
||||
this.dst = this.closestShoreTileToTarget(this.target, this.cell)
|
||||
this.path = this.computePath(this.src, this.dst)
|
||||
if (this.path != null) {
|
||||
console.log(`got path ${this.path.map(t => t.cell().toString())}`)
|
||||
this.boat = this.attacker.addBoat(1000, this.src.cell(), this.target)
|
||||
} else {
|
||||
console.log('got null path')
|
||||
this.active = false
|
||||
}
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (!this.active) {
|
||||
return
|
||||
}
|
||||
if (ticks - this.lastMove < this.ticksPerMove) {
|
||||
return
|
||||
}
|
||||
this.lastMove = ticks
|
||||
this.currTileIndex++
|
||||
|
||||
if (this.currTileIndex >= this.path.length) {
|
||||
if (this.dst.owner() == this.attacker) {
|
||||
this.attacker.addTroops(this.troops)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
this.attacker.conquer(this.dst.cell())
|
||||
this.mg.addExecution(new AttackExecution(this.troops, this.attacker.id(), this.targetID, null))
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
|
||||
const nextTile = this.path[this.currTileIndex]
|
||||
this.boat.move(nextTile.cell())
|
||||
}
|
||||
|
||||
owner(): MutablePlayer {
|
||||
return this.attacker
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
|
||||
private closestShoreTileToTarget(player: Player, target: Cell): Tile {
|
||||
const shoreTiles = Array.from(player.borderTiles()).filter(t => t.onShore())
|
||||
|
||||
return shoreTiles.reduce((closest, current) => {
|
||||
const closestDistance = manhattanDist(target, closest.cell());
|
||||
const currentDistance = manhattanDist(target, current.cell());
|
||||
return currentDistance < closestDistance ? current : closest;
|
||||
});
|
||||
}
|
||||
|
||||
private computePath(src: Tile, dst: Tile): Tile[] {
|
||||
if (!src.onShore() || !dst.onShore()) {
|
||||
return null; // Both source and destination must be on water
|
||||
}
|
||||
|
||||
const openSet = new PriorityQueue<{tile: Tile, fScore: number}>(
|
||||
11,
|
||||
(a, b) => a.fScore - b.fScore
|
||||
);
|
||||
const cameFrom = new Map<Tile, Tile>();
|
||||
const gScore = new Map<Tile, number>();
|
||||
|
||||
gScore.set(src, 0);
|
||||
openSet.add({tile: src, fScore: this.heuristic(src, dst)});
|
||||
|
||||
while (!openSet.empty()) {
|
||||
const current = openSet.poll()!.tile;
|
||||
|
||||
if (current === dst) {
|
||||
return this.reconstructPath(cameFrom, current);
|
||||
}
|
||||
|
||||
for (const neighbor of current.neighbors()) {
|
||||
if (!neighbor.onShore()) continue; // Skip non-water tiles
|
||||
|
||||
const tentativeGScore = gScore.get(current)! + 1; // Assuming uniform cost
|
||||
|
||||
if (!gScore.has(neighbor) || tentativeGScore < gScore.get(neighbor)!) {
|
||||
cameFrom.set(neighbor, current);
|
||||
gScore.set(neighbor, tentativeGScore);
|
||||
const fScore = tentativeGScore + this.heuristic(neighbor, dst);
|
||||
|
||||
openSet.add({tile: neighbor, fScore: fScore});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // No path found
|
||||
}
|
||||
|
||||
private heuristic(a: Tile, b: Tile): number {
|
||||
// Manhattan distance
|
||||
return Math.abs(a.cell().x - b.cell().x) + Math.abs(a.cell().y - b.cell().y);
|
||||
}
|
||||
|
||||
private reconstructPath(cameFrom: Map<Tile, Tile>, current: Tile): Tile[] {
|
||||
const path = [current];
|
||||
while (cameFrom.has(current)) {
|
||||
current = cameFrom.get(current)!;
|
||||
path.unshift(current);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {Cell, Execution, MutableGame, MutablePlayer, PlayerID, PlayerInfo} from "../Game"
|
||||
import {PseudoRandom} from "../PseudoRandom"
|
||||
import {AttackExecution} from "./AttackExecution";
|
||||
|
||||
export class BotExecution implements Execution {
|
||||
private ticks = 0
|
||||
|
||||
private active = true
|
||||
private random: PseudoRandom;
|
||||
private attackRate: number
|
||||
private gs: MutableGame
|
||||
|
||||
constructor(private bot: MutablePlayer) {
|
||||
|
||||
this.random = new PseudoRandom(bot.id())
|
||||
this.attackRate = this.random.nextInt(100, 500)
|
||||
}
|
||||
|
||||
init(gs: MutableGame, ticks: number) {
|
||||
this.gs = gs
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (!this.bot.isAlive()) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
|
||||
this.ticks++
|
||||
|
||||
if (this.ticks % this.attackRate == 0) {
|
||||
const ns = this.bot.neighbors()
|
||||
if (ns.length == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const toAttack = ns[this.random.nextInt(0, ns.length)]
|
||||
|
||||
this.gs.addExecution(new AttackExecution(
|
||||
this.bot.troops() / 5,
|
||||
this.bot.id(),
|
||||
toAttack.isPlayer() ? toAttack.id() : null,
|
||||
null
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
owner(): MutablePlayer {
|
||||
return this.bot
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import {Cell, Game, TerrainTypes} from "../Game";
|
||||
import {PseudoRandom} from "../PseudoRandom";
|
||||
import {SpawnIntent} from "../Schemas";
|
||||
import {getSpawnCells} from "./Util";
|
||||
|
||||
|
||||
export class BotSpawner {
|
||||
private cellToIndex;
|
||||
private freeTiles: Cell[];
|
||||
private numFreeTiles;
|
||||
private random = new PseudoRandom(123);
|
||||
|
||||
constructor(private gs: Game) { }
|
||||
|
||||
spawnBots(numBots: number): SpawnIntent[] {
|
||||
const bots: SpawnIntent[] = [];
|
||||
this.cellToIndex = new Map<string, number>();
|
||||
this.freeTiles = new Array();
|
||||
this.numFreeTiles = 0;
|
||||
|
||||
this.gs.forEachTile(tile => {
|
||||
if (tile.terrain() == TerrainTypes.Water) {
|
||||
return;
|
||||
}
|
||||
if (tile.hasOwner()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.freeTiles.push(tile.cell());
|
||||
this.cellToIndex.set(tile.cell().toString(), this.numFreeTiles);
|
||||
this.numFreeTiles++;
|
||||
});
|
||||
for (let i = 0; i < numBots; i++) {
|
||||
bots.push(this.spawnBot("Bot" + i));
|
||||
}
|
||||
return bots;
|
||||
}
|
||||
|
||||
spawnBot(botName: string): SpawnIntent {
|
||||
const rand = this.random.nextInt(0, this.numFreeTiles);
|
||||
const spawn = this.freeTiles[rand];
|
||||
const spawnCells = getSpawnCells(this.gs, spawn);
|
||||
spawnCells.forEach(c => this.removeCell(c));
|
||||
const spawnIntent: SpawnIntent = {
|
||||
type: 'spawn',
|
||||
name: botName,
|
||||
isBot: true,
|
||||
x: spawn.x,
|
||||
y: spawn.y
|
||||
};
|
||||
return spawnIntent;
|
||||
}
|
||||
|
||||
private removeCell(cell: Cell) {
|
||||
const index = this.cellToIndex[cell.toString()];
|
||||
this.freeTiles[index] = this.freeTiles[this.numFreeTiles - 1];
|
||||
this.cellToIndex[this.freeTiles[index].toString()] = index;
|
||||
this.numFreeTiles--;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import PriorityQueue from "priority-queue-typescript";
|
||||
import {Cell, Execution, MutableGame, Game, MutablePlayer, PlayerInfo, TerraNullius, Tile} from "../Game";
|
||||
import {AttackIntent, BoatAttackIntentSchema, Intent, Turn} from "../Schemas";
|
||||
import {AttackExecution} from "./AttackExecution";
|
||||
import {SpawnExecution} from "./SpawnExecution";
|
||||
import {BotSpawner} from "./BotSpawner";
|
||||
import {BoatAttackExecution} from "./BoatAttackExecution";
|
||||
|
||||
|
||||
export class Executor {
|
||||
|
||||
constructor(private gs: Game) {
|
||||
|
||||
}
|
||||
|
||||
addTurn(turn: Turn) {
|
||||
turn.intents.forEach(i => this.addIntent(i))
|
||||
}
|
||||
|
||||
addIntent(intent: Intent) {
|
||||
if (intent.type == "attack") {
|
||||
this.gs.addExecution(
|
||||
new AttackExecution(
|
||||
intent.troops,
|
||||
intent.attackerID,
|
||||
intent.targetID,
|
||||
new Cell(intent.targetX, intent.targetY)
|
||||
)
|
||||
)
|
||||
} else if (intent.type == "spawn") {
|
||||
this.gs.addExecution(
|
||||
new SpawnExecution(
|
||||
new PlayerInfo(intent.name, intent.isBot),
|
||||
new Cell(intent.x, intent.y),
|
||||
)
|
||||
)
|
||||
} else if (intent.type == "boat") {
|
||||
this.gs.addExecution(
|
||||
new BoatAttackExecution(
|
||||
intent.attackerID,
|
||||
intent.targetID,
|
||||
new Cell(intent.x, intent.y),
|
||||
intent.troops,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
throw new Error(`intent type ${intent} not found`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
spawnBots(numBots: number): void {
|
||||
new BotSpawner(this.gs).spawnBots(numBots).forEach(i => this.addIntent(i))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {Execution, MutableGame, MutablePlayer, PlayerID} from "../Game"
|
||||
|
||||
export class PlayerExecution implements Execution {
|
||||
|
||||
private player: MutablePlayer
|
||||
|
||||
constructor(private playerID: PlayerID) {
|
||||
}
|
||||
|
||||
init(gs: MutableGame, ticks: number) {
|
||||
this.player = gs.player(this.playerID)
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
this.player.addTroops(Math.sqrt(this.player.numTilesOwned() * this.player.troops() + 1000) / 1000)
|
||||
}
|
||||
|
||||
owner(): MutablePlayer {
|
||||
return this.player
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.player.isAlive()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import {Cell, Execution, MutableGame, MutablePlayer, PlayerInfo} from "../Game"
|
||||
import {BotExecution} from "./BotExecution"
|
||||
import {PlayerExecution} from "./PlayerExecution"
|
||||
import {getSpawnCells} from "./Util"
|
||||
|
||||
export class SpawnExecution implements Execution {
|
||||
|
||||
active: boolean = true
|
||||
private gs: MutableGame
|
||||
|
||||
constructor(
|
||||
private playerInfo: PlayerInfo,
|
||||
private cell: Cell,
|
||||
) { }
|
||||
|
||||
|
||||
init(gs: MutableGame, ticks: number) {
|
||||
this.gs = gs
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (!this.isActive()) {
|
||||
return
|
||||
}
|
||||
const player = this.gs.addPlayer(this.playerInfo)
|
||||
getSpawnCells(this.gs, this.cell).forEach(c => {
|
||||
console.log('conquering cell')
|
||||
player.conquer(c)
|
||||
})
|
||||
this.gs.addExecution(new PlayerExecution(player.id()))
|
||||
if (player.info().isBot) {
|
||||
this.gs.addExecution(new BotExecution(player))
|
||||
}
|
||||
this.active = false
|
||||
}
|
||||
owner(): MutablePlayer {
|
||||
return null
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {Game, Cell, TerrainTypes} from "../Game";
|
||||
|
||||
|
||||
export function getSpawnCells(gs: Game, cell: Cell): Cell[] {
|
||||
let result: Cell[] = [];
|
||||
for (let dx = -2; dx <= 2; dx++) {
|
||||
for (let dy = -2; dy <= 2; dy++) {
|
||||
let c = new Cell(cell.x + dx, cell.y + dy);
|
||||
if (!gs.isOnMap(c)) {
|
||||
continue;
|
||||
}
|
||||
if (Math.abs(dx) === 2 && Math.abs(dy) === 2) {
|
||||
continue;
|
||||
}
|
||||
if (gs.tile(c).terrain() != TerrainTypes.Land) {
|
||||
continue;
|
||||
}
|
||||
if (gs.tile(c).hasOwner()) {
|
||||
continue;
|
||||
}
|
||||
result.push(c);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user