Skip to content
This repository was archived by the owner on Oct 24, 2021. It is now read-only.

Commit 8d4c7c8

Browse files
CarlosFdezgithub-actions[bot]TespaHoishin
authored
feat: additional tracking (#91)
* feat: track turn duration * refactor: discover events moved to a single parser class * feat: implemented discover history * feat: track match length * fix(data): update cards.json (#90) Co-authored-by: Tespa <[email protected]> * fix(data): update cards.json (#92) Co-authored-by: Tespa <[email protected]> * fix(data): update cards.json (New data for Darkmoon) (#93) * fix(data): update cards.json * feat: update secrets.ts for new cards Co-authored-by: Tespa <[email protected]> Co-authored-by: Keiichiro Amemiya <[email protected]> * feat: corruption and partial refactor Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tespa <[email protected]> Co-authored-by: Keiichiro Amemiya <[email protected]>
1 parent 7ac8c14 commit 8d4c7c8

20 files changed

+9129
-379
lines changed

src/GameState.ts

Lines changed: 129 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,31 @@ const UNKNOWN_CARDNAME = 'UNKNOWN ENTITY [cardType=INVALID]';
88
* Tests if a card name is empty or the "empty string"
99
* @param cardName
1010
*/
11-
const isEmpty = (cardName: string) => {
11+
const isEmpty = (cardName?: string) => {
1212
return !cardName || cardName === UNKNOWN_CARDNAME;
1313
};
1414

15+
/**
16+
* Returns an array of special tags that can be used to communicate additional properties of a card.
17+
* Currently the available properties deal with corruption.
18+
* @param entity
19+
*/
20+
const identifySpecialTags = (entity: CardEntity | undefined) => {
21+
if (!entity || !entity.tags) {
22+
return;
23+
}
24+
25+
// Tags are handled here. These are the "simplified" version.
26+
const tags = new Array<EntityTags>();
27+
if (entity.tags.CORRUPTEDCARD === '1') {
28+
tags.push('corrupt');
29+
} else if (entity.tags.CORRUPT === '1') {
30+
tags.push('can-corrupt');
31+
}
32+
33+
return tags.length > 0 ? tags : undefined;
34+
};
35+
1536
export interface Secret {
1637
cardId: string;
1738
cardClass: Class;
@@ -34,52 +55,91 @@ export interface Card {
3455
/**
3556
* ID used by logs to distinguish same cards
3657
*/
37-
cardEntityId: number;
58+
entityId: number;
59+
3860
/**
3961
* Numeric ID for the card (same for same card)
4062
* Unknown card has this undefined
4163
*/
4264
cardId?: number;
65+
4366
/**
4467
* Unknown card has this undefined
4568
*/
4669
cardName?: string;
70+
4771
state: CardState;
72+
4873
/**
4974
* If card is originally from the deck
5075
*/
5176
readonly isSpawnedCard: boolean;
77+
78+
/**
79+
* Additional tags for when cards have special types
80+
*/
81+
tags?: EntityTags[];
82+
}
83+
84+
type EntityTags = 'can-corrupt' | 'corrupt';
85+
86+
export interface EntityProps {
87+
entityId: number;
88+
cardId?: number;
89+
cardName: string;
90+
player: 'top' | 'bottom';
91+
damage?: number;
92+
healing?: number;
93+
dead?: boolean;
94+
95+
/**
96+
* Additional tags for when cards have special types
97+
*/
98+
tags?: EntityTags[];
99+
}
100+
101+
/**
102+
* Extracts a subset of entity data for presentational use.
103+
* @param entity CardEntity to pull data from
104+
*/
105+
export const simplifyEntity = (entity: CardEntity): EntityProps => {
106+
return {
107+
cardId: entity.cardId,
108+
cardName: entity.cardName,
109+
entityId: entity.entityId,
110+
player: entity.player
111+
};
112+
};
113+
114+
export interface Discovery {
115+
enabled: boolean;
116+
id: string | null;
117+
source?: EntityProps;
118+
chosen?: EntityProps;
119+
options: EntityProps[];
52120
}
53121

54122
export interface Player {
55123
id: number;
56124
name: string;
57125
status: 'LOST' | 'WON' | 'TIED' | '';
58126
turn: boolean;
127+
turnHistory: Array<{
128+
startTime: number;
129+
duration?: number;
130+
}>;
59131
quests: Quest[];
60132
timeout: number;
61133
cardCount: number;
62134
cards: Card[];
63135
position: 'top' | 'bottom';
64136
secrets: Secret[];
65-
discovery: {
66-
enabled: boolean;
67-
id: string | null;
68-
};
137+
discovery: Discovery;
138+
discoverHistory: Discovery[];
69139
cardsReplacedInMulligan: number;
70140
manaSpent: number;
71141
}
72142

73-
export interface EntityProps {
74-
cardId?: number;
75-
cardName: string;
76-
entityId: number;
77-
player: 'top' | 'bottom';
78-
damage?: number;
79-
healing?: number;
80-
dead?: boolean;
81-
}
82-
83143
type MatchLogType = 'attack' | 'play' | 'trigger';
84144

85145
export class MatchLogEntry {
@@ -117,25 +177,54 @@ export class MatchLogEntry {
117177
this.targets.push(this.createProps(entity, ...props));
118178
}
119179

180+
/**
181+
* Marks targets/sources using the death entries.
182+
* Returns the entity ids of the cards that were successfully marked.
183+
* @param deaths Entity IDs that need to be marked
184+
* @returns the subset of deaths that were present in this log entry
185+
*/
186+
markDeaths(deaths: Set<number>) {
187+
const marked = new Set<number>();
188+
189+
if (deaths.has(this.source.entityId)) {
190+
this.source.dead = true;
191+
marked.add(this.source.entityId);
192+
}
193+
194+
for (const target of this.targets) {
195+
if (deaths.has(target.entityId)) {
196+
target.dead = true;
197+
marked.add(target.entityId);
198+
}
199+
}
200+
201+
return marked;
202+
}
203+
120204
private createProps(entity: CardEntity, ...props: Array<Partial<EntityProps> | undefined>) {
121-
return merge({
122-
cardId: entity.cardId,
123-
cardName: entity.cardName,
124-
entityId: entity.entityId,
125-
player: entity.player
126-
}, ...props);
205+
const tags = identifySpecialTags(entity);
206+
const merged = merge(simplifyEntity(entity), ...props);
207+
if (tags) {
208+
merged.tags = tags;
209+
}
210+
211+
return merged;
127212
}
128213
}
129214

130215
export class GameState {
131216
startTime: number;
132217

218+
matchDuration: number;
219+
133220
playerCount: number;
134221

135222
gameOverCount: number;
136223

137224
players: Player[];
138225

226+
beginPhaseActive: boolean;
227+
139228
mulliganActive: boolean;
140229

141230
turnStartTime: Date;
@@ -177,6 +266,7 @@ export class GameState {
177266
reset(): void {
178267
this.players = [];
179268
this.matchLog = [];
269+
this.beginPhaseActive = true;
180270
this.gameOverCount = 0;
181271
this.#entities = {};
182272
this.#missingEntityIds.clear();
@@ -248,22 +338,34 @@ export class GameState {
248338
* this handles the name resolution. Recommended place is the TAG_CHANGE event.
249339
* @param entity
250340
*/
251-
resolveEntity(entity: Pick<CardEntity, 'cardName' | 'entityId' | 'cardId'> & Partial<CardEntity>) {
341+
resolveEntity(entity: Pick<CardEntity, 'entityId'> & Partial<CardEntity>) {
252342
const existing = this.#entities[entity.entityId];
253-
this.#entities[entity.entityId] = merge({
343+
const newEntity = merge({
254344
type: 'card',
255345
tags: {},
256-
player: 'bottom'
346+
player: 'bottom',
347+
cardName: ''
257348
}, existing, entity);
258349

259-
const {cardName, entityId, cardId} = entity;
350+
this.#entities[entity.entityId] = newEntity;
351+
const {cardName, entityId, cardId} = newEntity;
260352
const newProps = {entityId, cardName, cardId};
261353

354+
// Update player cards in case this entity updated any important tags (like corrupt)
355+
for (const player of this.players) {
356+
for (const card of player.cards.filter(c => c.entityId === entity.entityId)) {
357+
const tags = identifySpecialTags(this.#entities[card.entityId]);
358+
if (tags) {
359+
card.tags = tags;
360+
}
361+
}
362+
}
363+
262364
if (isEmpty(cardName) || !this.#missingEntityIds.has(entityId)) {
263365
return;
264366
}
265367

266-
// Update entities for each log entry
368+
// Update entities for each match log entry (only card name, entity id, and card id)
267369
for (const entry of this.matchLog) {
268370
if (isEmpty(entry.source.cardName) && entry.source.entityId === entityId) {
269371
entry.source = {...entry.source, ...newProps};

src/line-parsers/AbstractLineParser.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {Events, HspEventsEmitter} from './index';
66
* Root class of all classes that read lines and emit events.
77
*/
88
export abstract class LineParser {
9-
abstract readonly eventName: keyof Events;
9+
abstract readonly eventName: string;
1010

1111
// eslint-disable-next-line @typescript-eslint/member-ordering
1212
private _logger: debug.IDebugger;
@@ -27,10 +27,10 @@ export abstract class LineParser {
2727
}
2828

2929
/**
30-
* Regex based parser class that exists for backwards compatibility.
31-
* Not a recommended way of solving it.
30+
* Regex based parser class used to handle one time events
3231
*/
3332
export abstract class AbstractLineParser extends LineParser {
33+
abstract readonly eventName: keyof Events;
3434
abstract readonly regex: RegExp;
3535

3636
// eslint-disable-next-line @typescript-eslint/member-ordering

src/line-parsers/card-init.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {LineParser} from './AbstractLineParser';
2+
import {HspEventsEmitter} from './index';
3+
import {GameState} from '../GameState';
4+
import {createSimpleRegexParser, FullEntityReader} from './readers';
5+
6+
/**
7+
* Handles when entities are first created to add the entries to the gamestate.
8+
* Does nothing once mulligan phase has ended.
9+
*/
10+
export class CardInitParser extends LineParser {
11+
eventName = 'card-init';
12+
13+
private readonly beginPhaseEndReader = createSimpleRegexParser(
14+
/\[LoadingScreen\] MulliganManager.HandleGameStart\(\) - IsPastBeginPhase\(\)=False/,
15+
_parts => ({ended: true})
16+
);
17+
18+
private readonly fullEntityReader = new FullEntityReader('[Power] GameState.DebugPrintPower() -');
19+
20+
handleLine(_emitter: HspEventsEmitter, gameState: GameState, line: string): boolean {
21+
// If this executes, the begin phase is done
22+
if (this.beginPhaseEndReader(line)) {
23+
gameState.beginPhaseActive = false;
24+
return true;
25+
}
26+
27+
// Does nothing if mulligan is inactive
28+
// We should consider somehow detecting if we're in a block (aka only top level)
29+
if (gameState.active && gameState.beginPhaseActive) {
30+
const fullEntityResult = this.fullEntityReader.handleLine(line, gameState);
31+
if (fullEntityResult.result) {
32+
gameState.resolveEntity(fullEntityResult.result.entity);
33+
return true;
34+
}
35+
}
36+
37+
return false;
38+
}
39+
}

src/line-parsers/choice-id.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/line-parsers/discovery-end.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

src/line-parsers/discovery-start.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

0 commit comments

Comments
 (0)