Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fog improvement: Introduce unit visibility range #1855

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions agot-bg-game-server/src/client/GameSettingsComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import LobbyGameState from "../common/lobby-game-state/LobbyGameState";
import { OverlayTrigger, Tooltip } from "react-bootstrap";
import { allGameSetups, getGameSetupContainer } from "../common/ingame-game-state/game-data-structure/createGame";
import IngameGameState from "../common/ingame-game-state/IngameGameState";
import { isMobile } from "react-device-detect";

interface GameSettingsComponentProps {
gameClient: GameClient;
Expand Down Expand Up @@ -394,14 +395,22 @@ export default class GameSettingsComponent extends Component<GameSettingsCompone
label={
<OverlayTrigger overlay={
<Tooltip id="custom-balancing-tooltip">
A community proposal to {this.props.entireGame.isMotherOfDragons ?
"avoid an early gang up against Targaryen" :
"improve Tyrell's starting position"}. For details see<br/>
<a href="https://community.swordsandravens.net/viewtopic.php?t=6" target="_blank" rel="noopener noreferrer">
Tex&apos;s balance proposal
</a>.
{this.props.entireGame.isMotherOfDragons
? <>
A community proposal to avoid an early gang up against Targaryen. For
details see<br/>
<a href="https://www.boardgamedungeon.net/threads/my-balance-proposition-for-a-game-of-thrones-mother-of-dragons-expansion-targaryen.11/" target="_blank" rel="noopener noreferrer">
Tex&apos;s balance proposal
</a>.
</>
: <>
A community proposal to improve Tyrell&apos;s starting position by
adding a ship to Redwyne Straight&apos;s.
</>}
</Tooltip>}
delay={{show: 0, hide: 1500}}>
delay={{show: 0, hide: 1500}}
placement={isMobile ? "auto" : "bottom"}
>
<label htmlFor="custom-balancing-setting">Custom Balancing</label>
</OverlayTrigger>}
checked={this.gameSettings.customBalancing}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default class WesterosGameStateComponent extends Component<GameStateCompo
[ShiftingAmbitionsGameState, ShiftingAmbitionsComponent],
[NewInformationGameState, NewInformationComponent]
])}
{this.props.gameState.childGameState instanceof DarkWingsDarkWordsGameState && (
{this.props.gameState.childGameState instanceof DarkWingsDarkWordsGameState && !this.props.gameState.ingame.fogOfWar && (
<Row className="mt-3 justify-content-center">
<PossiblePowerTokenGainsComponent ingame={this.props.gameState.ingame} />
</Row>
Expand Down
23 changes: 12 additions & 11 deletions agot-bg-game-server/src/common/EntireGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ export default class EntireGame extends GameState<null, LobbyGameState | IngameG
onCaptureSentryMessage?: (message: string, severity: "info" | "warning" | "error" | "fatal") => void;
onSaveGame?: (updateLastActive: boolean) => void;
onGetUser?: (userId: string) => Promise<StoredUserData | null>;
onBeforeGameStateChangedTransmitted?: () => void;

// Throttled saveGame so we don't spam the website client
saveGame: (updateLastActive: boolean) => void = _.throttle(this.privateSaveGame, 2000);
Expand Down Expand Up @@ -174,10 +173,10 @@ export default class EntireGame extends GameState<null, LobbyGameState | IngameG
// console.log("===GAME STATE CHANGED===");
// The GameState tree has been changed, broadcast a message to transmit to them
// the new game state.
if (this.ingameGameState) {
this.ingameGameState.updateVisibleRegions();
}
this.broadcastCustomToClients(u => {
if (this.onBeforeGameStateChangedTransmitted != null) {
this.onBeforeGameStateChangedTransmitted();
}
// To serialize the specific game state that has changed, the code serializes the entire
// game state tree and pick the appropriate serializedGameState.
// TODO: Find less wasteful way of doing this
Expand Down Expand Up @@ -222,6 +221,9 @@ export default class EntireGame extends GameState<null, LobbyGameState | IngameG
p.resetWaitedFor();
});

if (this.ingameGameState) {
this.ingameGameState.updateVisibleRegions(true);
}
this.notifyWaitedUsers();
return true;
}
Expand Down Expand Up @@ -642,8 +644,8 @@ export default class EntireGame extends GameState<null, LobbyGameState | IngameG
getPlayersInGame(): {userId: string; data: object}[] {
// eslint-disable-next-line @typescript-eslint/ban-types
const players: {userId: string; data: object}[] = [];
if (this.childGameState instanceof LobbyGameState) {
this.childGameState.players.forEach((user, house) => {
if (this.lobbyGameState) {
this.lobbyGameState.players.forEach((user, house) => {
// If the game is in "randomize house" mode, don't specify any houses in the PlayerInGame data
const playerData: {[key: string]: any} = {};

Expand All @@ -656,11 +658,10 @@ export default class EntireGame extends GameState<null, LobbyGameState | IngameG
data: playerData
});
});
} else if (this.childGameState instanceof IngameGameState) {
const ingame = this.childGameState as IngameGameState;
const waitedForUsers = ingame.getWaitedUsers();
} else if (this.ingameGameState) {
const waitedForUsers = this.ingameGameState.getWaitedUsers();

ingame.players.forEach((player, user) => {
this.ingameGameState.players.forEach((player, user) => {
// "Important chat rooms" are chat rooms where unseen messages will display
// a badge next to the game in the website.
// In this case, it's all private rooms with this player in it. The next line
Expand All @@ -674,7 +675,7 @@ export default class EntireGame extends GameState<null, LobbyGameState | IngameG
"house": player.house.id,
"waited_for": waitedForUsers.includes(user),
"important_chat_rooms": importantChatRooms.map(cr => cr.roomId),
"is_winner": ingame.childGameState instanceof GameEndedGameState ? ingame.childGameState.winner == player.house : false,
"is_winner": this.ingameGameState!.childGameState instanceof GameEndedGameState ? this.ingameGameState!.childGameState.winner == player.house : false,
"needed_for_vote": player.isNeededForVote
}
});
Expand Down
75 changes: 43 additions & 32 deletions agot-bg-game-server/src/common/ingame-game-state/IngameGameState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export default class IngameGameState extends GameState<
@observable ordersOnBoard: BetterMap<Region, Order> = new BetterMap();
@observable visibleRegionsPerPlayer: BetterMap<Player, Region[]> = new BetterMap();
@observable publicVisibleRegions: Region[] = [];
unitVisibilityRange = 1;
unitVisibilityRangeModifier = 0;

votes: BetterMap<string, Vote> = new BetterMap();
@observable paused: Date | null = null;
Expand Down Expand Up @@ -132,10 +132,6 @@ export default class IngameGameState extends GameState<

constructor(entireGame: EntireGame) {
super(entireGame);

entireGame.onBeforeGameStateChangedTransmitted = () => {
this.updateVisibleRegions();
}
}

beginGame(housesToCreate: string[], futurePlayers: BetterMap<string, User>): void {
Expand Down Expand Up @@ -248,7 +244,6 @@ export default class IngameGameState extends GameState<
proceedPlanningGameState(planningRestrictions: PlanningRestriction[] = [], revealedWesterosCards: WesterosCard[] = []): void {
this.game.vassalRelations = new BetterMap();
this.broadcastVassalRelations();
this.updateVisibleRegions(true);
this.setChildGameState(new PlanningGameState(this)).firstStart(planningRestrictions, revealedWesterosCards);
}

Expand Down Expand Up @@ -337,7 +332,7 @@ export default class IngameGameState extends GameState<
});

if (this.fogOfWar) {
this.unitVisibilityRange = 1;
this.unitVisibilityRangeModifier = 0;
this.publicVisibleRegions = [];
this.entireGame.users.values.filter(u => u.connected).forEach(u => {
this.entireGame.sendMessageToClients([u], {
Expand All @@ -348,7 +343,6 @@ export default class IngameGameState extends GameState<
applyChangesNow: !this.players.has(u)
});
});
this.updateVisibleRegions(true);
}

if (this.game.turn > 1) {
Expand Down Expand Up @@ -1660,6 +1654,11 @@ export default class IngameGameState extends GameState<
return this.visibleRegionsPerPlayer.get(player);
}

calculateVisibilityRangeForRegion(region: Region): number {
const baseRange = Math.max(...region.units.values.map(u => u.type.visibilityRange));
return Math.max(0, baseRange + this.unitVisibilityRangeModifier);
}

calculateVisibleRegionsForPlayer(player: Player | null): Region[] {
if (!this.fogOfWar || !player) {
return [];
Expand All @@ -1673,34 +1672,46 @@ export default class IngameGameState extends GameState<
const allRegionsWithControllers = this.world.getAllRegionsWithControllers();

// We begin with all controlled areas of own and vassal units. We definitely always see them
const result: Region[] = allRegionsWithControllers.filter(([_r, h]) => controlledHouses.includes(h)).map(([r, _h]) => r);
let regionsWithUnits = result.filter(r => r.units.size > 0);
const checkedRegions: Region[] = [];
const result: Set<Region> = new Set(allRegionsWithControllers.filter(([_r, h]) => controlledHouses.includes(h)).map(([r, _h]) => r));
const regionsWithUnits = Array.from(result).filter(r => r.units.size > 0);
const checkedRegions = new Set<Region>();

// Additionally we see regions adjacents to our regions with units
for(let i=0; i < this.unitVisibilityRange; i++) {
const additionalRegionsToCheck: Region[] = [];
for (let j=0; j<regionsWithUnits.length; j++) {
const region = regionsWithUnits[j];
let adjacent: Region[] = [];
if (!checkedRegions.includes(region)) {
adjacent = this.world.getNeighbouringRegions(region);
result.push(...adjacent);
additionalRegionsToCheck.push(...adjacent)
checkedRegions.push(region);
}
}
for (let i = 0; i < regionsWithUnits.length; i++) {
const rootRegion = regionsWithUnits[i];

const visibilityRange = this.calculateVisibilityRangeForRegion(rootRegion);
let rootRegions = [rootRegion];
for (let j = 0; j < visibilityRange; j++) {
const allAdjacents = new Set<Region>();
for (let k = 0; k < rootRegions.length; k++) {
const region = rootRegions[k];
if (checkedRegions.has(region)) {
continue;
}

if (this.unitVisibilityRange > 1) {
regionsWithUnits.push(...additionalRegionsToCheck);
regionsWithUnits = _.uniq(regionsWithUnits);
const adjacent = this.world.getNeighbouringRegions(region);
adjacent.forEach(r => {
allAdjacents.add(r);
result.add(r);
});
checkedRegions.add(region);
}
rootRegions = Array.from(allAdjacents);
}
}

result.push(...this.calculateRequiredVisibleRegionsForPlayer(player));
result.push(...this.publicVisibleRegions)
[...this.calculateRequiredVisibleRegionsForPlayer(player), ...this.publicVisibleRegions].forEach(r => result.add(r));

return _.uniq(result);
// Add ports of visible castles:
result.forEach(r => {
const port = this.world.getAdjacentPortOfCastle(r);
if (port) {
result.add(port);
}
});

return Array.from(result);
}

calculateRequiredVisibleRegionsForPlayer(player: Player): Region[] {
Expand Down Expand Up @@ -2308,7 +2319,7 @@ export default class IngameGameState extends GameState<
? this.visibleRegionsPerPlayer.entries.map(([p, regions]) => [p.user.id, regions.map(r => r.id)])
: this.visibleRegionsPerPlayer.entries.filter(([p, _regions]) => p.user == user).map(([p, regions]) => [p.user.id, regions.map(r => r.id)]),
publicVisibleRegions: this.publicVisibleRegions.map(r => r.id),
unitVisibilityRange: this.unitVisibilityRange,
unitVisibilityRangeModifier: this.unitVisibilityRangeModifier,
oldPlayerIds: this.oldPlayerIds,
replacerIds: this.replacerIds,
timeoutPlayerIds: this.timeoutPlayerIds,
Expand All @@ -2335,7 +2346,7 @@ export default class IngameGameState extends GameState<
data.visibleRegionsPerPlayer.map(([uid, rids]) => [ingameGameState.players.get(entireGame.users.get(uid)), rids.map(rid => ingameGameState.world.regions.get(rid))])
);
ingameGameState.publicVisibleRegions = data.publicVisibleRegions.map(rid => ingameGameState.world.regions.get(rid));
ingameGameState.unitVisibilityRange = data.unitVisibilityRange;
ingameGameState.unitVisibilityRangeModifier = data.unitVisibilityRangeModifier;
ingameGameState.oldPlayerIds = data.oldPlayerIds;
ingameGameState.replacerIds = data.replacerIds;
ingameGameState.timeoutPlayerIds = data.timeoutPlayerIds;
Expand Down Expand Up @@ -2382,7 +2393,7 @@ export interface SerializedIngameGameState {
players: SerializedPlayer[];
visibleRegionsPerPlayer: [string, string[]][];
publicVisibleRegions: string[];
unitVisibilityRange: number;
unitVisibilityRangeModifier: number;
oldPlayerIds: string[];
replacerIds: string[];
timeoutPlayerIds: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,6 @@ export default class PostCombatGameState extends GameState<
applyChangesNow: !this.combat.ingameGameState.players.has(u)
});
});
this.combat.ingameGameState.updateVisibleRegions(true);
}

this.combat.resolveMarchOrderGameState.onResolveSingleMarchOrderGameStateFinish(this.attacker);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default class UnitType {
walksOn: RegionKind;
canTransport: RegionKind | null;
canRetreat: boolean;
visibilityRange: number;

constructor(
id: string,
Expand All @@ -18,7 +19,8 @@ export default class UnitType {
combatStrength: number,
combatStrengthOnAttackStructure: number | null = null,
canTransport: RegionKind | null = null,
canRetreat = true
canRetreat = true,
visibilityRange = 1
) {
this.id = id;
this.name = name;
Expand All @@ -28,5 +30,6 @@ export default class UnitType {
this.combatStrength = combatStrength;
this.combatStrengthOnAttackStructure = combatStrengthOnAttackStructure;
this.canRetreat = canRetreat;
this.visibilityRange = visibilityRange;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const footman = new UnitType("footman", "Footman", "Adds 1 Combat Strengt
export const knight = new UnitType("knight", "Knight", "Adds 2 Combat Strength in battle. Costs 2 points of mustering (or 1 point if upgraded from a Footman).", RegionKind.LAND, 2);
export const siegeEngine = new UnitType("siege-engine", "Siege Engine", "Adds 4 Combat Strength when attacking (or supporting an attack against) an area containing a Castle or Stronghold, otherwise it adds 0. Siege Engines may not retreat when losing combat; they are destroyed instead. Costs 2 points of mustering (or 1 point if upgraded from a Footman).", RegionKind.LAND, 0, 4, null, false);
export const ship = new UnitType("ship", "Ship", "Adds 1 Combat Strength in battle. Costs 1 point of mustering.", RegionKind.SEA, 1, null, RegionKind.LAND);
export const dragon = new UnitType("dragon", "Dragon", "Adds 0-5 Combat Strength in battle, depending on the current position of the dragon strength token. Dragons are extraordinarily rare creatures and cannot be mustered like regular units. Instead, they are in play from the beginning of the game.", RegionKind.LAND, 0);
export const dragon = new UnitType("dragon", "Dragon", "Adds 0-5 Combat Strength in battle, depending on the current position of the dragon strength token. Dragons are extraordinarily rare creatures and cannot be mustered like regular units. Instead, they are in play from the beginning of the game.", RegionKind.LAND, 0, null, null, true, 2);

const unitTypes = new BetterMap<string, UnitType>([
[footman.id, footman],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import WesterosGameState from "../../westeros-game-state/WesterosGameState";

export default class DenseFogWesterosCardType extends WesterosCardType {
execute(westeros: WesterosGameState): void {
westeros.ingame.unitVisibilityRange = 0;
westeros.ingame.unitVisibilityRangeModifier = -1;
westeros.ingame.updateVisibleRegions(true);
westeros.onWesterosCardEnd();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export default class Vote {
checkVoteFinished(): void {
if (this.state == VoteState.ACCEPTED) {
this.type.executeAccepted(this);
this.ingame.updateVisibleRegions();
this.ingame.updateVisibleRegions(true);
}
}

Expand Down
14 changes: 14 additions & 0 deletions agot-bg-game-server/src/server/serializedGameMigrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2391,6 +2391,20 @@ const serializedGameMigrations: {version: string; migrate: (serializeGamed: any)
serializedGame.gameSettings.houseCardsEvolutionRound = 5;
return serializedGame;
}
},
{
version: "122",
migrate: (serializedGame: any) => {
if (serializedGame.childGameState.type == "ingame") {
const ingame = serializedGame.childGameState;
if (ingame.unitVisibilityRange === undefined) {
ingame.unitVisibilityRangeModifier = 0;
} else {
ingame.unitVisibilityRangeModifier = ingame.unitVisibilityRange - 1;
}
}
return serializedGame;
}
}
];

Expand Down