multithreading: radial menu works

This commit is contained in:
evanpelle
2025-01-07 16:40:18 -08:00
committed by Evan
parent 9a95fbe89e
commit 7673cf03ac
8 changed files with 228 additions and 195 deletions
+103 -3
View File
@@ -3,11 +3,12 @@ import { getConfig } from "./configuration/Config";
import { EventBus } from "./EventBus";
import { Executor } from "./execution/ExecutionManager";
import { WinCheckExecution } from "./execution/WinCheckExecution";
import { Game, MutableGame, MutableTile, PlayerID, Tile, TileEvent } from "./game/Game";
import { Cell, DisplayMessageEvent, Game, MessageType, MutableGame, MutableTile, Player, PlayerID, Tile, TileEvent, UnitType } from "./game/Game";
import { createGame } from "./game/GameImpl";
import { loadTerrainMap } from "./game/TerrainMapLoader";
import { GameUpdateViewData, NameViewData, packTileData, PlayerViewData } from "./GameView";
import { GameUpdateViewData, NameViewData, packTileData, PlayerActions, PlayerViewData } from "./GameView";
import { GameConfig, Turn } from "./Schemas";
import { and, bfs, dist, targetTransportTile } from "./Util";
export async function createGameRunner(gameID: string, gameConfig: GameConfig, callBack: (gu: GameUpdateViewData) => void): Promise<GameRunner> {
const config = getConfig(gameConfig)
@@ -90,4 +91,103 @@ export class GameRunner {
this.isExecuting = false
}
}
public playerActions(playerID: PlayerID, x: number, y: number): PlayerActions {
const player = this.game.player(playerID)
const tile = this.game.tile(new Cell(x, y))
const actions = {
canBoat: this.canBoat(player, tile),
canAttack: this.canAttack(player, tile),
buildableUnits: Object.values(UnitType).filter(ut => player.canBuild(ut, tile) != false)
} as PlayerActions
if (tile.hasOwner()) {
const other = tile.owner() as Player
actions.interaction = {
sharedBorder: player.sharesBorderWith(other),
canSendEmoji: player.canSendEmoji(other),
canTarget: player.canTarget(other),
canSendAllianceRequest: !player.recentOrPendingAllianceRequestWith(other),
canBreakAlliance: player.isAlliedWith(other),
canDonate: player.canDonate(other)
}
}
return actions
}
private canBoat(myPlayer: Player, tile: Tile): boolean {
const other = tile.owner()
if (myPlayer.units(UnitType.TransportShip).length >= this.game.config().boatMaxNumber()) {
return false
}
let myPlayerBordersOcean = false
for (const bt of myPlayer.borderTiles()) {
if (bt.terrain().isOceanShore()) {
myPlayerBordersOcean = true
break
}
}
let otherPlayerBordersOcean = false
if (!tile.hasOwner()) {
otherPlayerBordersOcean = true
} else {
for (const bt of (other as Player).borderTiles()) {
if (bt.terrain().isOceanShore()) {
otherPlayerBordersOcean = true
break
}
}
}
if (other.isPlayer() && myPlayer.allianceWith(other)) {
return false
}
let nearOcean = false
for (const t of bfs(tile, and(t => t.owner() == tile.owner() && t.terrain().isLand(), dist(tile, 25)))) {
if (t.terrain().isOceanShore()) {
nearOcean = true
break
}
}
if (!nearOcean) {
return false
}
if (myPlayerBordersOcean && otherPlayerBordersOcean) {
const dst = targetTransportTile(this.game.width(), tile)
if (dst != null) {
if (myPlayer.canBuild(UnitType.TransportShip, dst)) {
return true
}
}
}
}
private canAttack(myPlayer: Player, tile: Tile): boolean {
if (tile.owner() == myPlayer) {
return false
}
// TODO: fix event bus
if (tile.owner().isPlayer() && myPlayer.isAlliedWith(tile.owner() as Player)) {
this.eventBus.emit(new DisplayMessageEvent("Cannot attack ally", MessageType.WARN))
return false
}
if (!tile.terrain().isLand()) {
return false
}
if (tile.hasOwner()) {
return myPlayer.sharesBorderWith(tile.owner())
} else {
for (const t of bfs(tile, and(t => !t.hasOwner() && t.terrain().isLand(), dist(tile, 200)))) {
for (const n of t.neighbors()) {
if (n.owner() == myPlayer) {
return true
}
}
}
return false
}
}
}
+21 -5
View File
@@ -124,9 +124,29 @@ export interface PlayerViewData extends ViewData<PlayerViewData> {
targetTroopRatio: number
}
export interface PlayerActions {
canBoat: boolean
canAttack: boolean
buildableUnits: UnitType[]
interaction?: PlayerInteraction
}
export interface PlayerInteraction {
sharedBorder: boolean
canSendEmoji: boolean
canSendAllianceRequest: boolean
canBreakAlliance: boolean
canTarget: boolean
canDonate: boolean
}
export class PlayerView implements Player {
constructor(private game: GameView, public data: PlayerViewData) { }
async actions(tile: Tile): Promise<PlayerActions> {
return this.game.worker.playerInteraction(this.id(), tile)
}
nameLocation(): NameViewData {
return this.data.nameViewData
}
@@ -196,10 +216,6 @@ export class PlayerView implements Player {
return false
}
async sharesBorderWithAsync(other: Player | TerraNullius): Promise<boolean> {
return this.game.worker.sharesBorderWith(this.id(), other.id())
}
incomingAllianceRequests(): AllianceRequest[] {
return []
}
@@ -325,7 +341,7 @@ export class GameView {
return false
}
playerViews(): PlayerView[] {
return Object.values(this.lastUpdate.players).map(data => new PlayerView(this, data))
return Array.from(this._players.values())
}
players(): Player[] {
-2
View File
@@ -19,8 +19,6 @@ import { DestroyerExecution } from "./DestroyerExecution";
import { PortExecution } from "./PortExecution";
import { MissileSiloExecution } from "./MissileSiloExecution";
import { BattleshipExecution } from "./BattleshipExecution";
import { PathFinder } from "../pathfinding/PathFinding";
import { WorkerClient } from "../worker/WorkerClient";
import { DefensePostExecution } from "./DefensePostExecution";
import { CityExecution } from "./CityExecution";
+6 -9
View File
@@ -4,7 +4,7 @@ import {
MainThreadMessage,
WorkerMessage,
InitializedMessage,
SharesBorderResultMessage
PlayerActionsResultMessage,
} from './WorkerMessages';
const ctx: Worker = self as any;
@@ -58,21 +58,18 @@ ctx.addEventListener('message', async (e: MessageEvent<MainThreadMessage>) => {
}
break;
case 'shares_border':
case 'player_actions':
if (!gameRunner) {
throw new Error('Game runner not initialized');
}
try {
const game = (await gameRunner).game
const result = game.player(message.player1)
.sharesBorderWith(game.player(message.player2))
const actions = (await gameRunner).playerActions(message.playerID, message.x, message.y)
sendMessage({
type: 'shares_border_result',
type: 'player_actions_result',
id: message.id,
result
} as SharesBorderResultMessage);
result: actions
} as PlayerActionsResultMessage);
} catch (error) {
console.error('Failed to check borders:', error);
throw error;
+10 -8
View File
@@ -1,5 +1,5 @@
import { PlayerID } from "../game/Game";
import { GameUpdateViewData } from "../GameView";
import { PlayerID, Tile } from "../game/Game";
import { GameUpdateViewData, PlayerActions, PlayerInteraction } from "../GameView";
import { GameConfig, GameID, Turn } from "../Schemas";
import { generateID } from "../Util";
import { WorkerMessage } from "./WorkerMessages";
@@ -29,7 +29,7 @@ export class WorkerClient {
break;
case 'initialized':
case 'shares_border_result':
case 'player_actions_result':
if (message.id && this.messageHandlers.has(message.id)) {
const handler = this.messageHandlers.get(message.id)!;
handler(message);
@@ -85,7 +85,7 @@ export class WorkerClient {
});
}
sharesBorderWith(p1: PlayerID, p2: PlayerID): Promise<boolean> {
playerInteraction(playerID: PlayerID, tile: Tile): Promise<PlayerActions> {
return new Promise((resolve, reject) => {
if (!this.isInitialized) {
reject(new Error('Worker not initialized'));
@@ -95,21 +95,23 @@ export class WorkerClient {
const messageId = generateID()
this.messageHandlers.set(messageId, (message) => {
if (message.type === 'shares_border_result' && message.result !== undefined) {
if (message.type === 'player_actions_result' && message.result !== undefined) {
resolve(message.result);
}
});
this.worker.postMessage({
type: 'shares_border',
type: 'player_actions',
id: messageId,
player1: p1,
player2: p2
playerID: playerID,
x: tile.cell().x,
y: tile.cell().y
});
});
}
cleanup() {
this.worker.terminate();
this.messageHandlers.clear();
+17 -14
View File
@@ -1,4 +1,4 @@
import { GameUpdateViewData } from "../GameView";
import { GameUpdateViewData, PlayerActions, PlayerInteraction } from "../GameView";
import { GameConfig, GameID, Turn } from "../Schemas";
import { PlayerID } from "../game/Game";
@@ -7,8 +7,8 @@ export type WorkerMessageType =
| 'initialized'
| 'turn'
| 'game_update'
| 'shares_border'
| 'shares_border_result';
| 'player_actions'
| 'player_actions_result';
// Base interface for all messages
interface BaseWorkerMessage {
@@ -28,12 +28,6 @@ export interface TurnMessage extends BaseWorkerMessage {
turn: Turn;
}
export interface SharesBorderMessage extends BaseWorkerMessage {
type: 'shares_border';
player1: PlayerID;
player2: PlayerID;
}
// Messages from worker to main thread
export interface InitializedMessage extends BaseWorkerMessage {
type: 'initialized';
@@ -44,11 +38,20 @@ export interface GameUpdateMessage extends BaseWorkerMessage {
gameUpdate: GameUpdateViewData;
}
export interface SharesBorderResultMessage extends BaseWorkerMessage {
type: 'shares_border_result';
result: boolean;
export interface PlayerActionsMessage extends BaseWorkerMessage {
type: 'player_actions'
playerID: PlayerID
x: number,
y: number
}
export interface PlayerActionsResultMessage extends BaseWorkerMessage {
type: 'player_actions_result';
result: PlayerActions;
}
// Union types for type safety
export type MainThreadMessage = InitMessage | TurnMessage | SharesBorderMessage;
export type WorkerMessage = InitializedMessage | GameUpdateMessage | SharesBorderResultMessage;
export type MainThreadMessage = InitMessage | TurnMessage | PlayerActionsMessage
// Message send from worker
export type WorkerMessage = InitializedMessage | GameUpdateMessage | PlayerActionsResultMessage;