-
Notifications
You must be signed in to change notification settings - Fork 5
/
Battle.ts
185 lines (165 loc) · 5.88 KB
/
Battle.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import { inspect } from "util";
import { Logger } from "../../Logger";
import { RequestMessage } from "../dispatcher/Message";
import { MessageListener } from "../dispatcher/MessageListener";
import { Choice } from "./Choice";
import { EventProcessor, EventProcessorConstructor } from "./EventProcessor";
/**
* Sends a Choice to the server.
* @param choice Choice to send.
*/
export type ChoiceSender = (choice: Choice) => void;
/** Constructs an abstract Battle type. */
export interface BattleConstructor
{
new(username: string, listener: MessageListener, sender: ChoiceSender,
logger: Logger): BattleBase;
}
/**
* Contains public members from the Battle class. Used for polymorphism without
* having to supply a template argument.
*/
export abstract class BattleBase {}
/**
* Manages the entire course of a battle in the client's point of view.
* @template Processor Type of EventProcessor to use.
*/
export abstract class Battle<Processor extends EventProcessor>
extends BattleBase
{
/** Manages the BattleState by processing events. */
protected readonly processor: Processor;
/** Logger object. */
protected readonly logger: Logger;
/** Last |request| message that was processed. */
protected lastRequest: RequestMessage;
/** Available choices from the last decision. */
protected lastChoices: Choice[] = [];
/** Used to send the AI's choice to the server. */
private readonly sender: ChoiceSender;
/**
* Creates a Battle object.
* @param username Client's username.
* @param listener Used to subscribe to server messages.
* @param sender Used to send the AI's choice to the server.
* @param processor Type of EventProcessor to use.
* @param logger Logger object.
*/
constructor(username: string, listener: MessageListener,
sender: ChoiceSender, processor: EventProcessorConstructor<Processor>,
logger: Logger)
{
super();
this.processor = new processor(username, logger);
this.sender = sender;
this.logger = logger;
listener
.on("battleinit", args =>
{
logger.debug(`battleinit:
${inspect(args, {colors: false, depth: null})}`);
this.processor.initBattle(args);
this.processor.printState();
return this.askAI();
})
.on("battleprogress", async args =>
{
logger.debug(`battleprogress:
${inspect(args, {colors: false, depth: null})}`);
// last best choice was officially accepted by the server
this.acceptChoice(this.lastChoices[0]);
this.processor.handleEvents(args.events);
this.processor.printState();
if (this.processor.battling)
{
this.processor.printState();
if (!this.lastRequest.wait) await this.askAI();
this.processor.postAction();
}
})
.on("request", args =>
{
logger.debug(`request:
${inspect(args, {colors: false, depth: null})}`);
this.processor.handleRequest(args);
this.lastRequest = args;
})
.on("callback", args =>
{
if (args.name === "trapped")
{
// last choice is invalid because we're trapped now
// avoid repeated callback messages by eliminating all switch
// choices
// TODO: use this to imply trapped in BattleState so this
// doesn't happen multiple times
this.lastChoices = this.lastChoices
.filter(c => !c.startsWith("switch"));
}
// first choice was rejected
else this.lastChoices.shift();
// retry using second choice
// TODO: re-decide instead of iterate if new info is found
this.sender(this.lastChoices[0]);
});
}
/**
* Decides what to do next.
* @param choices The set of possible choices that can be made.
* @returns A Promise to sort the given choices in order of preference.
*/
protected abstract decide(choices: Choice[]): Promise<Choice[]>;
/**
* Called when the server has officially accepted the Battle instance's
* Choice decision.
* @virtual
*/
protected acceptChoice(choice: Choice): void
{
}
/** Asks the AI what to do next and sends the response. */
private async askAI(): Promise<void>
{
const choices = this.getChoices();
this.logger.debug(`choices: [${choices.join(", ")}]`);
this.lastChoices = await this.decide(choices);
this.sender(this.lastChoices[0]);
}
/**
* Determines what choices can be made.
* @returns A list of choices that can be made by the AI.
*/
private getChoices(): Choice[]
{
const choices: Choice[] = [];
if (!this.lastRequest.forceSwitch && this.lastRequest.active)
{
// not forced to switch so we can move
const moves = this.lastRequest.active[0].moves;
let struggle = true;
for (let i = 0; i < moves.length; ++i)
{
if (!moves[i].disabled)
{
choices.push(`move ${i + 1}` as Choice);
struggle = false;
}
}
// allow struggle choice if no other move option
if (struggle) choices.push("move 1");
}
if (!this.lastRequest.active || !this.lastRequest.active[0].trapped)
{
// not trapped so we can switch
const mons = this.lastRequest.side.pokemon;
for (let i = 0; i < mons.length; ++i)
{
if (mons[i].condition.hp !== 0 && !mons[i].active)
{
choices.push(`switch ${i + 1}` as Choice);
}
}
}
return choices;
}
}