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
+10 -38
View File
@@ -190,45 +190,17 @@ export class ClientGameRunner {
return
}
}
const owner = tile.owner()
const targetID = owner.isPlayer() ? owner.id() : null;
if (tile.owner() == this.myPlayer) {
return
}
if (tile.owner().isPlayer() && this.myPlayer.isAlliedWith(tile.owner() as Player)) {
this.eventBus.emit(new DisplayMessageEvent("Cannot attack ally", MessageType.WARN))
return
}
if (tile.terrain().isLand()) {
if (tile.hasOwner()) {
this.myPlayer.sharesBorderWithAsync(tile.owner()).then(sharesBorder => {
if (sharesBorder) {
this.eventBus.emit(
new SendAttackIntentEvent(
targetID,
this.myPlayer.troops() * this.renderer.uiState.attackRatio
)
)
}
})
} else {
outer_loop: 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().isPlayer()) {
console.log(`owner: ${(n.owner() as PlayerView).name()}`)
}
if (n.owner() == this.myPlayer) {
this.eventBus.emit(new SendAttackIntentEvent(targetID, this.myPlayer.troops() * this.renderer.uiState.attackRatio))
break outer_loop
}
}
}
this.myPlayer.actions(tile).then(actions => {
console.log(`got actions: ${JSON.stringify(actions)}`)
if (actions.canAttack) {
this.eventBus.emit(
new SendAttackIntentEvent(
tile.owner().id(),
this.myPlayer.troops() * this.renderer.uiState.attackRatio
)
)
}
}
})
}
}
+61 -116
View File
@@ -1,5 +1,5 @@
import { EventBus } from "../../../../core/EventBus";
import { AllPlayers, Cell, Game, Player, UnitType } from "../../../../core/game/Game";
import { AllPlayers, Cell, Game, Player, Tile, UnitType } from "../../../../core/game/Game";
import { ClientID } from "../../../../core/Schemas";
import { and, bfs, dist, manhattanDist, manhattanDistWrapped, sourceDstOceanShore, targetTransportTile } from "../../../../core/Util";
import { ContextMenuEvent, MouseUpEvent, ShowBuildMenuEvent } from "../../../InputHandler";
@@ -20,7 +20,7 @@ import { EmojiTable } from "./EmojiTable";
import { UIState } from "../../UIState";
import { BuildMenu } from "./BuildMenu";
import { consolex } from "../../../../core/Consolex";
import { GameView } from "../../../../core/GameView";
import { GameView, PlayerActions, PlayerView } from "../../../../core/GameView";
enum Slot {
@@ -237,7 +237,6 @@ export class RadialMenu implements Layer {
return
}
const tile = this.game.tile(this.clickedCell)
const other = tile.owner()
if (this.game.inSpawnPhase()) {
if (tile.terrain().isLand() && !tile.hasOwner()) {
@@ -246,134 +245,82 @@ export class RadialMenu implements Layer {
return
}
const myPlayer = this.game.players().find(p => p.clientID() == this.clientID)
const myPlayer = this.game.playerViews().find(p => p.clientID() == this.clientID)
if (!myPlayer) {
consolex.warn('my player not found')
return
}
myPlayer.actions(tile).then(actions => {
this.handlePlayerActions(myPlayer, actions, tile)
})
}
private handlePlayerActions(myPlayer: PlayerView, actions: PlayerActions, tile: Tile) {
this.activateMenuElement(Slot.Build, "#ebe250", buildIcon, () => {
this.buildMenu.showMenu(myPlayer, this.clickedCell)
})
if (tile.hasOwner()) {
const target = tile.owner() == myPlayer ? AllPlayers : (tile.owner() as Player)
if (myPlayer.canSendEmoji(target)) {
this.activateMenuElement(Slot.Emoji, "#00a6a4", emojiIcon, () => {
this.emojiTable.onEmojiClicked = (emoji: string) => {
this.emojiTable.hideTable()
this.eventBus.emit(new SendEmojiIntentEvent(target, emoji))
}
this.emojiTable.showTable()
})
}
}
if (tile.owner() != myPlayer && tile.terrain().isLand() && myPlayer.sharesBorderWith(other)) {
if (other.isPlayer()) {
if (!myPlayer.isAlliedWith(other)) {
this.enableCenterButton(true)
if (actions.interaction?.canSendEmoji) {
this.activateMenuElement(Slot.Emoji, "#00a6a4", emojiIcon, () => {
const target = tile.owner() == myPlayer ? AllPlayers : (tile.owner() as Player)
this.emojiTable.onEmojiClicked = (emoji: string) => {
this.emojiTable.hideTable()
this.eventBus.emit(new SendEmojiIntentEvent(target, emoji))
}
} else {
outer_loop: 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) {
this.enableCenterButton(true)
break outer_loop
}
}
}
}
this.emojiTable.showTable()
})
}
if (tile.hasOwner()) {
const other = tile.owner() as Player
if (other.clientID() == this.clientID) {
return
}
if (myPlayer.canDonate(other)) {
this.activateMenuElement(Slot.Target, "#53ac75", donateIcon, () => {
this.eventBus.emit(
new SendDonateIntentEvent(myPlayer, other, null)
if (actions.canBoat) {
this.activateMenuElement(Slot.Boat, "#3f6ab1", boatIcon, () => {
this.eventBus.emit(
new SendBoatAttackIntentEvent(
myPlayer.id(),
this.clickedCell,
this.uiState.attackRatio * myPlayer.troops()
)
})
}
if (myPlayer.isAlliedWith(other)) {
this.activateMenuElement(Slot.Alliance, "#c74848", traitorIcon, () => {
this.eventBus.emit(
new SendBreakAllianceIntentEvent(myPlayer, other)
)
})
} else if (!myPlayer.recentOrPendingAllianceRequestWith(other)) {
this.activateMenuElement(Slot.Alliance, "#53ac75", allianceIcon, () => {
this.eventBus.emit(
new SendAllianceRequestIntentEvent(myPlayer, other)
)
})
}
if (myPlayer.canTarget(other)) {
this.activateMenuElement(Slot.Target, "#c74848", targetIcon, () => {
this.eventBus.emit(
new SendTargetPlayerIntentEvent(other.id())
)
})
}
)
})
}
if (actions.canAttack) {
this.enableCenterButton(true)
}
if (!tile.terrain().isLand()) {
return
}
if (myPlayer.units(UnitType.TransportShip).length >= this.game.config().boatMaxNumber()) {
return
}
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
}
const other = tile.owner() as Player
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
if (actions?.interaction.canDonate) {
this.activateMenuElement(Slot.Target, "#53ac75", donateIcon, () => {
this.eventBus.emit(
new SendDonateIntentEvent(myPlayer, other, null)
)
})
}
if (myPlayerBordersOcean && otherPlayerBordersOcean) {
const dst = targetTransportTile(this.game.width(), tile)
if (dst != null) {
if (myPlayer.canBuild(UnitType.TransportShip, dst)) {
this.activateMenuElement(Slot.Boat, "#3f6ab1", boatIcon, () => {
this.eventBus.emit(
new SendBoatAttackIntentEvent(other.id(), this.clickedCell, this.uiState.attackRatio * myPlayer.troops())
)
})
}
}
if (actions?.interaction.canTarget) {
this.activateMenuElement(Slot.Target, "#c74848", targetIcon, () => {
this.eventBus.emit(
new SendTargetPlayerIntentEvent(other.id())
)
})
}
if (actions?.interaction.canSendAllianceRequest) {
this.activateMenuElement(Slot.Alliance, "#53ac75", allianceIcon, () => {
this.eventBus.emit(
new SendAllianceRequestIntentEvent(myPlayer, other)
)
})
}
if (actions?.interaction.canBreakAlliance) {
this.activateMenuElement(Slot.Alliance, "#c74848", traitorIcon, () => {
this.eventBus.emit(
new SendBreakAllianceIntentEvent(myPlayer, other)
)
})
}
}
@@ -408,11 +355,9 @@ export class RadialMenu implements Layer {
if (this.game.inSpawnPhase()) {
this.eventBus.emit(new SendSpawnIntentEvent(this.clickedCell))
} else {
if (clicked.owner().clientID() != this.clientID) {
const myPlayer = this.game.players().find(p => p.clientID() == this.clientID)
if (myPlayer != null) {
this.eventBus.emit(new SendAttackIntentEvent(clicked.owner().id(), this.uiState.attackRatio * myPlayer.troops()))
}
const myPlayer = this.game.players().find(p => p.clientID() == this.clientID)
if (myPlayer != null && clicked.owner() != myPlayer) {
this.eventBus.emit(new SendAttackIntentEvent(clicked.owner().id(), this.uiState.attackRatio * myPlayer.troops()))
}
}
this.hideRadialMenu();
+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;