Skip to content

Commit

Permalink
[*] Make dice and dice rolls more type safe
Browse files Browse the repository at this point in the history
  • Loading branch information
LeCodex committed Dec 30, 2024
1 parent 5416196 commit 89a32bb
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 69 deletions.
10 changes: 5 additions & 5 deletions src/oath/game/actions/effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -748,11 +748,11 @@ export class RecoverTargetEffect extends PlayerEffect {
}
}

export class RollDiceEffect extends OathEffect<RollResult> {
export class RollDiceEffect<T extends Die<any>> extends OathEffect<RollResult<T>> {
amount: number;
result: RollResult;
result: RollResult<T>;

constructor(game: OathGame, player: OathPlayer | undefined, dieOrResult: typeof Die | RollResult, amount: number) {
constructor(game: OathGame, player: OathPlayer | undefined, dieOrResult: T | RollResult<T>, amount: number) {
super(game, player);
this.amount = Math.max(0, amount);
this.result = dieOrResult instanceof RollResult ? dieOrResult : new RollResult(game.random, dieOrResult);
Expand All @@ -766,7 +766,7 @@ export class RollDiceEffect extends OathEffect<RollResult> {
serialize() {
return {
...super.serialize(),
die: this.result.die.name,
die: this.result.die.constructor.name,
result: Object.fromEntries(this.result.symbols.entries())
};
}
Expand Down Expand Up @@ -903,7 +903,7 @@ export class NextTurnEffect extends OathEffect {
}

if (this.game.round > 5 && this.game.oathkeeper.isImperial) {
new RollDiceEffect(this.game, this.game.chancellor, D6, 1).doNext(result => {
new RollDiceEffect(this.game, this.game.chancellor, new D6(), 1).doNext(result => {
const threshold = [6, 5, 3][this.game.round - 6] ?? 7;
if (result.value >= threshold)
return this.game.empireWins();
Expand Down
12 changes: 6 additions & 6 deletions src/oath/game/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { OathAction, ModifiableAction, InvalidActionResolution, ChooseModifiers } from "./base";
import { Denizen, Edifice, OathCard, OwnableCard, Relic, Site, WorldCard } from "../cards";
import { DiscardOptions, SearchableDeck } from "../cards/decks";
import { AttackDie, DefenseDie, DieSymbol, RollResult } from "../dice";
import { AttackDie, AttackDieSymbol, DefenseDie, RollResult } from "../dice";
import { MoveResourcesToTargetEffect, PayCostToTargetEffect, PlayWorldCardEffect, RollDiceEffect, DrawFromDeckEffect, PutPawnAtSiteEffect, DiscardCardEffect, MoveOwnWarbandsEffect, SetPeoplesFavorMobState, ChangePhaseEffect, NextTurnEffect, PutResourcesOnTargetEffect, SetUsurperEffect, BecomeCitizenEffect, BecomeExileEffect, BuildEdificeFromDenizenEffect, WinGameEffect, FlipEdificeEffect, BindingExchangeEffect, CitizenshipOfferEffect, PeekAtCardEffect, TakeReliquaryRelicEffect, CheckCapacityEffect, CampaignJoinDefenderAlliesEffect, MoveWorldCardToAdvisersEffect, DiscardCardGroupEffect, ParentToTargetEffect, PaySupplyEffect, ThingsExchangeOfferEffect, SiteExchangeOfferEffect, SearchDrawEffect } from "./effects";
import { ALL_OATH_SUITS, ALL_PLAYER_COLORS, CardRestriction, OathPhase, OathSuit, OathType, PlayerColor } from "../enums";
import { OathGame } from "../game";
Expand Down Expand Up @@ -681,7 +681,7 @@ export class CampaignDefenseAction extends ModifiableAction {
this.campaignResult.successful = this.campaignResult.atk > this.campaignResult.def;

if (!this.campaignResult.ignoreKilling)
this.campaignResult.attackerKills(this.campaignResult.atkRoll.get(DieSymbol.Skull));
this.campaignResult.attackerKills(this.campaignResult.atkRoll.get(AttackDieSymbol.Skull));
});
}
}
Expand All @@ -707,8 +707,8 @@ export class CampaignResult {
defForce: Set<ResourcesAndWarbands>;
endCallbacks: CampaignEndCallback[] = [];

atkRoll: RollResult;
defRoll: RollResult;
atkRoll: RollResult<AttackDie>;
defRoll: RollResult<DefenseDie>;

ignoreKilling: boolean = false;
attackerKillsNoWarbands: boolean = false;
Expand All @@ -723,8 +723,8 @@ export class CampaignResult {

constructor(game: OathGame) {
this.game = game;
this.atkRoll = new RollResult(this.game.random, AttackDie);
this.defRoll = new RollResult(this.game.random, DefenseDie);
this.atkRoll = new RollResult(this.game.random, new AttackDie());
this.defRoll = new RollResult(this.game.random, new DefenseDie());
}

get winner() { return this.successful ? this.attacker : this.defender; }
Expand Down
83 changes: 44 additions & 39 deletions src/oath/game/dice.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
import { PRNG } from "./utils";

export enum DieSymbol {
Sword = 1,
TwoSwords = 2,
HollowSword = 0.5,
Skull = 0,
Shield = 1,
TwoShields = 2,
DoubleShield = -1
};

export abstract class Die {
static readonly faces: number[][];
export class Die<T extends number> {
readonly faces: T[][];

static getValue(symbols: Map<number, number>, ignore?: Set<number>): number {
getValue(symbols: Map<T, number>, ignore?: Set<T>): number {
let total = 0;
for (const [symbol, amount] of symbols.entries())
if (!ignore?.has(symbol))
Expand All @@ -23,32 +13,45 @@ export abstract class Die {
}
}

export class AttackDie extends Die {
static readonly faces = [
[DieSymbol.HollowSword],
[DieSymbol.HollowSword],
[DieSymbol.HollowSword],
[DieSymbol.Sword],
[DieSymbol.Sword],
[DieSymbol.TwoSwords, DieSymbol.Skull]
export enum AttackDieSymbol {
Sword = 1,
TwoSwords = 2,
HollowSword = 0.5,
Skull = 0,
};

export class AttackDie extends Die<AttackDieSymbol> {
readonly faces = [
[AttackDieSymbol.HollowSword],
[AttackDieSymbol.HollowSword],
[AttackDieSymbol.HollowSword],
[AttackDieSymbol.Sword],
[AttackDieSymbol.Sword],
[AttackDieSymbol.TwoSwords, AttackDieSymbol.Skull]
];
}

export class DefenseDie extends Die {
static readonly faces = [
export enum DefenseDieSymbol {
Shield = 1,
TwoShields = 2,
DoubleShield = -1,
}

export class DefenseDie extends Die<DefenseDieSymbol> {
readonly faces = [
[],
[],
[DieSymbol.Shield],
[DieSymbol.Shield],
[DieSymbol.TwoShields],
[DieSymbol.DoubleShield],
]
[DefenseDieSymbol.Shield],
[DefenseDieSymbol.Shield],
[DefenseDieSymbol.TwoShields],
[DefenseDieSymbol.DoubleShield],
];

static getValue(symbols: Map<number, number>, ignore?: Set<number>): number {
getValue(symbols: Map<DefenseDieSymbol, number>, ignore?: Set<DefenseDieSymbol>): number {
let total = 0, mult = 1;
for (const [symbol, amount] of symbols.entries()) {
if (ignore?.has(symbol)) continue;
if (symbol === DieSymbol.DoubleShield)
if (symbol === DefenseDieSymbol.DoubleShield)
mult *= 2;
else
total += amount * symbol;
Expand All @@ -57,23 +60,25 @@ export class DefenseDie extends Die {
}
}

export class D6 extends Die {
export class D6 extends Die<number> {
static readonly faces = [[1], [2], [3], [4], [5], [6]];
}

export type SymbolType<T extends Die<number>> = T extends Die<infer U> ? U : never;

/**
* The result of rolling some dice. Has an internal Map, with an ignore Set that forces the number and value of those faces to 0.
*
* get() returns 0 instead of undefined if a key isn't found.
*/
export class RollResult {
rolls: number[][] = [];
ignore: Set<number> = new Set();
export class RollResult<T extends Die<number>> {
rolls: SymbolType<T>[][] = [];
ignore: Set<SymbolType<T>> = new Set();

constructor(public random: PRNG, public die: typeof Die) { }
constructor(public random: PRNG, public die: T) { }

get symbols(): Map<number, number> {
const res = new Map<number, number>();
get symbols(): Map<SymbolType<T>, number> {
const res = new Map<SymbolType<T>, number>();
for (const roll of this.rolls)
for (const symbol of roll)
res.set(symbol, (res.get(symbol) ?? 0) + 1);
Expand All @@ -87,13 +92,13 @@ export class RollResult {

roll(amount: number): this {
for (let i = 0; i < amount; i++) {
const roll = this.random.pick(this.die.faces);
const roll = this.random.pick(this.die.faces) as SymbolType<T>[];
this.rolls.push(roll);
}
return this;
}

get(key: number): number {
get(key: SymbolType<T>): number {
if (this.ignore.has(key)) return 0;
return this.symbols.get(key) ?? 0;
}
Expand Down
10 changes: 5 additions & 5 deletions src/oath/game/powers/arcane.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CampaignAttackAction, CampaignDefenseAction, TakeFavorFromBankAction, TradeAction, TravelAction, MakeDecisionAction, RestAction, ChooseCardsAction, SearchPlayOrDiscardAction, ChoosePlayersAction, ChooseSitesAction, ChooseNumberAction, SearchAction, KillWarbandsOnTargetAction, MusterAction, RecoverBannerPitchAction, ChooseRnWsAction, RecoverAction } from "../actions";
import { Conspiracy, Denizen, Edifice, Relic, Site, WorldCard } from "../cards";
import { DiscardOptions } from "../cards/decks";
import { AttackDie, DefenseDie, DieSymbol, RollResult } from "../dice";
import { AttackDie, AttackDieSymbol, DefenseDie, RollResult } from "../dice";
import { RegionDiscardEffect, PutResourcesOnTargetEffect, RollDiceEffect, BecomeCitizenEffect, DiscardCardEffect, PeekAtCardEffect, MoveResourcesToTargetEffect, PutDenizenIntoDispossessedEffect, GetRandomCardFromDispossessed, MoveWorldCardToAdvisersEffect, ParentToTargetEffect, BurnResourcesEffect } from "../actions/effects";
import { BannerKey, OathSuit } from "../enums";
import { ExileBoard, OathPlayer } from "../player";
Expand Down Expand Up @@ -50,7 +50,7 @@ export class KindredWarriorsAttack extends AttackerBattlePlan<Denizen> {
cost = new ResourceCost([[Secret, 1]]);

applyBefore(): void {
this.action.campaignResult.atkRoll.ignore.add(DieSymbol.Skull);
this.action.campaignResult.atkRoll.ignore.add(AttackDieSymbol.Skull);
this.action.campaignResult.atkPool += (this.activator.ruledSuits - 1);
}
}
Expand Down Expand Up @@ -83,7 +83,7 @@ export class RustingRay extends DefenderBattlePlan<Denizen> {
applyBefore(): void {
const darkestSecretProxy = this.gameProxy.banners.get(BannerKey.DarkestSecret);
if (darkestSecretProxy?.owner !== this.activatorProxy) return;
this.action.campaignResult.atkRoll.ignore.add(DieSymbol.HollowSword);
this.action.campaignResult.atkRoll.ignore.add(AttackDieSymbol.HollowSword);
}
}

Expand Down Expand Up @@ -267,7 +267,7 @@ export class ActingTroupe extends AccessedActionModifier<Denizen, TradeAction> {
}
}

export class Jinx extends ActionModifier<Denizen, RollDiceEffect> {
export class Jinx extends ActionModifier<Denizen, RollDiceEffect<AttackDie | DefenseDie>> {
modifiedAction = RollDiceEffect;
cost = new ResourceCost([[Secret, 1]]);

Expand All @@ -279,7 +279,7 @@ export class Jinx extends ActionModifier<Denizen, RollDiceEffect> {
const result = this.action.result;
const player = this.action.executor;
if (!player) return;
if (this.action.result.die !== AttackDie || this.action.result.die !== DefenseDie) return;
if (!(this.action.result.die instanceof AttackDie) || !(this.action.result.die instanceof DefenseDie)) return;

new MakeDecisionAction(player, "Reroll " + this.action.result.rolls.map(e => e.join(" & ")).join(", ") + " ?", () => {
this.payCost(player, success => {
Expand Down
8 changes: 4 additions & 4 deletions src/oath/game/powers/beast.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { SearchAction, CampaignAttackAction, CampaignDefenseAction, TradeAction, TakeFavorFromBankAction, ActAsIfAtSiteAction, MakeDecisionAction, CampaignAction, ChoosePlayersAction, ChooseCardsAction, ChooseSuitsAction, KillWarbandsOnTargetAction, MusterAction, TravelAction, SearchPlayOrDiscardAction, BrackenAction } from "../actions";
import { InvalidActionResolution } from "../actions/base";
import { Denizen, Edifice, GrandScepter, Relic, Site } from "../cards";
import { DieSymbol } from "../dice";
import { BecomeCitizenEffect, DiscardCardEffect, DrawFromDeckEffect, FinishChronicleEffect, GainSupplyEffect, MoveDenizenToSiteEffect, MoveResourcesToTargetEffect, MoveWorldCardToAdvisersEffect, ParentToTargetEffect, PlayWorldCardEffect, RegionDiscardEffect, TakeOwnableObjectEffect } from "../actions/effects";
import { CardRestriction, OathSuit } from "../enums";
import { WithPowers } from "../interfaces";
import { ExileBoard, OathPlayer } from "../player";
import { Favor, Warband, ResourceCost, Secret } from "../resources";
import { AttackerBattlePlan, DefenderBattlePlan, WhenPlayed, RestPower, ActivePower, EnemyAttackerCampaignModifier, EnemyDefenderCampaignModifier, AccessedActionModifier, ActionModifier, EnemyActionModifier } from ".";
import { AttackDieSymbol, DefenseDieSymbol } from "../dice";


export class NatureWorshipAttack extends AttackerBattlePlan<Denizen> {
Expand All @@ -29,22 +29,22 @@ export class WarTortoiseAttack extends AttackerBattlePlan<Denizen> {
cost = new ResourceCost([[Favor, 1]]);

applyBefore(): void {
this.action.campaignResult.defRoll.ignore.add(DieSymbol.TwoShields);
this.action.campaignResult.defRoll.ignore.add(DefenseDieSymbol.TwoShields);
}
}
export class WarTortoiseDefense extends DefenderBattlePlan<Denizen> {
cost = new ResourceCost([[Favor, 1]]);

applyBefore(): void {
this.action.campaignResult.atkRoll.ignore.add(DieSymbol.TwoSwords);
this.action.campaignResult.atkRoll.ignore.add(AttackDieSymbol.TwoSwords);
}
}

export class Rangers extends AttackerBattlePlan<Denizen> {
cost = new ResourceCost([[Favor, 1]]);

applyBefore(): void {
this.action.campaignResult.atkRoll.ignore.add(DieSymbol.Skull);
this.action.campaignResult.atkRoll.ignore.add(AttackDieSymbol.Skull);
if (this.action.campaignResult.defPool >= 4) this.action.campaignResult.atkPool += 2;
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/oath/game/powers/discord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export class RelicThief extends EnemyActionModifier<Denizen, TakeOwnableObjectEf
const cost = new ResourceCost([[Favor, 1], [Secret, 1]]);
new PayCostToTargetEffect(this.game, rulerProxy.original, cost, this.source).doNext(success => {
if (!success) throw cost.cannotPayError;
new RollDiceEffect(this.game, rulerProxy.original, DefenseDie, 1).doNext(result => {
new RollDiceEffect(this.game, rulerProxy.original, new DefenseDie(), 1).doNext(result => {
if (result.value === 0) new TakeOwnableObjectEffect(this.game, rulerProxy.original, this.action.target).doNext();
});
});
Expand Down Expand Up @@ -234,7 +234,7 @@ export class GamblingHall extends ActivePower<Denizen> {
cost = new ResourceCost([[Favor, 2]]);

usePower(): void {
new RollDiceEffect(this.game, this.action.player, DefenseDie, 4).doNext(result => {
new RollDiceEffect(this.game, this.action.player, new DefenseDie(), 4).doNext(result => {
new TakeFavorFromBankAction(this.action.player, result.value).doNext();
});
}
Expand Down Expand Up @@ -531,12 +531,12 @@ export class FestivalDistrict extends ActivePower<Denizen> {
}
}

export class SqualidDistrict extends ActionModifier<Edifice, RollDiceEffect> {
export class SqualidDistrict extends ActionModifier<Edifice, RollDiceEffect<D6>> {
modifiedAction = RollDiceEffect;
mustUse = true;

canUse(): boolean {
return !!this.sourceProxy.ruler && this.action.result.die === D6;
return !!this.sourceProxy.ruler && this.action.result.die instanceof D6;
}

applyAfter(): void {
Expand Down
4 changes: 2 additions & 2 deletions src/oath/game/powers/hearth.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { TradeAction, TakeResourceFromPlayerAction, TakeFavorFromBankAction, CampaignEndAction, MakeDecisionAction, CampaignAttackAction, ChooseSuitsAction, ChooseCardsAction, MusterAction, SearchPlayOrDiscardAction, MayDiscardACardAction, SearchAction, SearchChooseAction, KillWarbandsOnTargetAction, TinkersFairOfferAction, StartBindingExchangeAction, DeedWriterOfferAction, CampaignEndCallback } from "../actions";
import { ModifiableAction, InvalidActionResolution } from "../actions/base";
import { Denizen, Edifice, Relic, WorldCard } from "../cards";
import { DieSymbol } from "../dice";
import { PlayVisionEffect, PlayWorldCardEffect, PeekAtCardEffect, DiscardCardEffect, BecomeCitizenEffect, SetPeoplesFavorMobState, GainSupplyEffect, DrawFromDeckEffect, TakeOwnableObjectEffect, MoveDenizenToSiteEffect, ParentToTargetEffect, PutResourcesOnTargetEffect, RecoverTargetEffect } from "../actions/effects";
import { BannerKey, OathSuit, ALL_OATH_SUITS } from "../enums";
import { WithPowers } from "../interfaces";
import { Favor, ResourceCost, Secret } from "../resources";
import { maxInGroup, minInGroup } from "../utils";
import { DefenderBattlePlan, AccessedActionModifier, ActivePower, WhenPlayed, EnemyActionModifier, AttackerBattlePlan, ActionModifier, EnemyDefenderCampaignModifier } from ".";
import { ExileBoard } from "../player";
import { AttackDieSymbol } from "../dice";


export class TravelingDoctorAttack extends AttackerBattlePlan<Denizen> {
Expand Down Expand Up @@ -52,7 +52,7 @@ export class TheGreatLevyAttack extends AttackerBattlePlan<Denizen> {
const peoplesFavorProxy = this.gameProxy.banners.get(BannerKey.PeoplesFavor);
if (peoplesFavorProxy?.owner?.original === this.action.campaignResult.defender) return;
this.action.campaignResult.atkPool += 3;
this.action.campaignResult.atkRoll.ignore.add(DieSymbol.Skull);
this.action.campaignResult.atkRoll.ignore.add(AttackDieSymbol.Skull);
}
}
export class TheGreatLevyDefense extends DefenderBattlePlan<Denizen> {
Expand Down
4 changes: 2 additions & 2 deletions src/oath/game/powers/nomad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { InvalidActionResolution, ModifiableAction, ResolveCallbackEffect } from
import { Region } from "../map";
import { Denizen, Edifice, OathCard, Relic, Site, VisionBack, WorldCard } from "../cards";
import { DiscardOptions } from "../cards/decks";
import { AttackDie, DieSymbol } from "../dice";
import { PayCostToTargetEffect, TakeOwnableObjectEffect, PutResourcesOnTargetEffect, PayPowerCostEffect, BecomeCitizenEffect, DrawFromDeckEffect, FlipEdificeEffect, MoveResourcesToTargetEffect, DiscardCardEffect, GainSupplyEffect, PutDenizenIntoDispossessedEffect, GetRandomCardFromDispossessed, PeekAtCardEffect, MoveWorldCardToAdvisersEffect, MoveDenizenToSiteEffect, DiscardCardGroupEffect, PlayVisionEffect, ParentToTargetEffect, BurnResourcesEffect, PutPawnAtSiteEffect, RecoverTargetEffect } from "../actions/effects";
import { BannerKey, OathSuit } from "../enums";
import { isOwnable } from "../interfaces";
import { ExileBoard, OathPlayer } from "../player";
import { Favor, ResourceCost, Secret } from "../resources";
import { ActivePower, CapacityModifier, AttackerBattlePlan, DefenderBattlePlan, WhenPlayed, EnemyAttackerCampaignModifier, EnemyActionModifier, ActionModifier, gainPowerUntilActionResolves, BattlePlan, AccessedActionModifier } from ".";
import { DefenseDieSymbol } from "../dice";


export class HorseArchersAttack extends AttackerBattlePlan<Denizen> {
Expand Down Expand Up @@ -126,7 +126,7 @@ export class WildMountsReplace extends ActionModifier<Denizen, DiscardCardEffect

export class RainBoots extends AttackerBattlePlan<Denizen> {
applyBefore(): void {
this.action.campaignResult.defRoll.ignore.add(DieSymbol.Shield);
this.action.campaignResult.defRoll.ignore.add(DefenseDieSymbol.Shield);
this.action.campaignResult.discardAtEnd(this.source);
}
}
Expand Down
Loading

0 comments on commit 89a32bb

Please sign in to comment.