Skip to content

Commit 533177f

Browse files
authored
Merge pull request #574 from SibylLab/Ai-version2.0-#573
AI for version 2.0
2 parents c236957 + 6355fb6 commit 533177f

17 files changed

Lines changed: 1095 additions & 22 deletions

src/classes/ai/ActionHandler.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @file ActionHandler.js file
3+
* @author Steven on 2020-06-10
4+
*/
5+
6+
/**
7+
* An abstract handler class to deal with possible AI player actions.
8+
* These are the chain of responsibility objects that can handle a
9+
* turn or not. If they can they pass a turn object back to AiHandler
10+
* and it passes it back to the client to take the turn, if the action handler
11+
* cannot handle the event it can return undefined and the AiHandler will
12+
* move to the next action handler.
13+
*/
14+
export default class ActionHandler {
15+
/**
16+
* Creates a new ActionHandler to be used when resolving AI turn choices.
17+
* Should not create actual instances of this class (though it is not forbidden).
18+
* @constructor ActionHandler
19+
* @param {Player} player The AI player the action handler is for.
20+
*/
21+
constructor (player) {
22+
this.player = player
23+
}
24+
25+
/**
26+
* Handles the request to take the ActionHandlers specific action.
27+
* Must be implemented in the subclasses.
28+
* @param hand The hand of the AI player.
29+
* @param players A list of all players in the game.
30+
* @param stacks A list of all stacks in play.
31+
* @param scores A list of current player scores.
32+
* @return a turn object {playType, card, player, target} or undefined.
33+
*/
34+
handle(hand, players, stacks, scores) { // eslint-disable-line no-unused-vars
35+
return undefined
36+
}
37+
}
38+

src/classes/ai/AiHandler.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* @file AiHandler.js file
3+
* @author Steven on 2020-06-10
4+
*/
5+
6+
import RedrawAction from '@/classes/ai/RedrawAction'
7+
8+
/**
9+
* A handler to take an Ai players turn.
10+
* Uses a pattern similar to Chain of Responsibility to make turn choices.
11+
* Consults objects in the actionHandlers list in order until one can
12+
* perform its action. If all actions fail it will take the default action
13+
* which should always be possible.
14+
*
15+
* Some more info aboout chain of responsibility can be found at:
16+
* https://refactoring.guru/design-patterns/chain-of-responsibility
17+
*/
18+
export default class AiHandler {
19+
/**
20+
* Creates a new AiHandler to make turn choices for a player.
21+
* @constructor AiHandler
22+
* @param {Player} player The Ai player this handler is for.
23+
*/
24+
constructor (player, handlers) {
25+
this.player = player
26+
this.actionHandlers = handlers
27+
this.defaultAction = new RedrawAction(this.player)
28+
}
29+
30+
/**
31+
* Make a choice for the player that it controls.
32+
* @param hand The hand of the Ai player.
33+
* @param players A list of all players in the game.
34+
* @param stacks A list of all stacks in play.
35+
* @param scores A list of current player scores.
36+
* @return a turn object {playType, card, player, target}
37+
*/
38+
chooseAction(hand, players, stacks, scores) {
39+
// Try all the action handlers until one returns a turn object
40+
for (let action of this.actionHandlers) {
41+
let result = action.handle(hand, players, stacks, scores)
42+
if (result) {
43+
return result
44+
}
45+
}
46+
// If no handler can handle this action use the default action
47+
return this.defaultAction.handle(hand, players, stacks, scores)
48+
}
49+
}

src/classes/ai/AiHandlerFactory.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* @file RedrawAction.js file
3+
* @author Steven on 2020-06-10
4+
*/
5+
6+
import AiHandler from '@/classes/ai/AiHandler'
7+
import PlayBestCardAction from '@/classes/ai/PlayBestCardAction'
8+
9+
10+
// card orders for different AI personalities
11+
const CARD_ORDER = {
12+
basic: ["VARIABLE", "REPEAT", "INSTRUCTION"],
13+
standard: [
14+
"GROUP", "VARIABLE", "REPEAT", "INSTRUCTION", "ANTIVIRUS", "FIREWALL",
15+
"OVERCLOCK", "HACK", "VIRUS"
16+
],
17+
aggressive: [
18+
"HACK", "VIRUS", "VARIABLE", "REPEAT", "INSTRUCTION", "GROUP",
19+
"FIREWALL", "ANTIVIRUS", "OVERCLOCK"
20+
],
21+
defensive: [
22+
"FIREWALL", "ANTIVIRUS", "OVERCLOCK", "GROUP", "VARIABLE", "REPEAT",
23+
"INSTRUCTION", "VIRUS", "HACK"
24+
]
25+
}
26+
27+
28+
/**
29+
* A factory to create different types of AiHandlers.
30+
*/
31+
export default class AiHandlerFactory {
32+
/**
33+
* @constructor AiHandlerFactory
34+
*/
35+
constructor () {}
36+
37+
/**
38+
* Create and return an AiHandler of the given type for the given player.
39+
*/
40+
newHandler (personality, player) {
41+
let actions = []
42+
43+
let cards = CARD_ORDER.basic
44+
if (personality in CARD_ORDER) {
45+
cards = CARD_ORDER[personality]
46+
}
47+
actions.push( new PlayBestCardAction(player, cards) )
48+
49+
let handler = new AiHandler(player, actions)
50+
return handler
51+
}
52+
}
53+
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
/**
2+
* @file PlayBestCardAction.js file
3+
* @author Steven on 2020-06-10
4+
*/
5+
6+
import ActionHandler from '@/classes/ai/ActionHandler'
7+
import helpers from '@/classes/ai/aiHelpers'
8+
9+
/**
10+
* Attempts to play cards from an AI players hand based on a given
11+
* piority. Each card has a reasonable but simple strategy for attempting
12+
* to play it.
13+
*
14+
* If a card type is not included in the priority it will not be played
15+
* even if it is in the hand. This make it possible to make more complicated
16+
* ActionHandlers that can be added to the AiHandler if desired.
17+
*/
18+
export default class PlayBestCardAction extends ActionHandler {
19+
/**
20+
* Creates a new PlayBestCardAction class
21+
* @constructor PlayBestCardAction
22+
* @param player The player that this handler is for.
23+
* @param playOrder A list of cards type in the order that they should be
24+
* considered for play. Types not in the order will never be played.
25+
*/
26+
constructor (player, playOrder) {
27+
super(player)
28+
this.playOrder = this.createOrder(playOrder)
29+
}
30+
31+
/**
32+
* Creates an object that maps card types to priorities to be used when
33+
* sorting the players hand.
34+
*/
35+
createOrder (playOrder) {
36+
let cardOrder = {}
37+
for (let i = 0; i < playOrder.length; i++) {
38+
cardOrder[playOrder[i]] = i
39+
}
40+
return cardOrder
41+
}
42+
43+
/**
44+
* Returns an move object for playing the higest priority card in the
45+
* players hand or undefined if none of them can be played.
46+
*
47+
* A mini chain of responsibility for cards that uses internal functions for
48+
* each card type for now.
49+
*/
50+
handle (hand, players, stacks, scores) {
51+
let cards = this.sortHand(hand)
52+
for (let card of cards) {
53+
let type = card.type.toLowerCase()
54+
55+
// Finding the correct method for this card type
56+
if (card.type in this.playOrder) {
57+
let move
58+
if (card.isSafety()) {
59+
move = this.playSafety(card)
60+
} else if (card.isAttack()) {
61+
move = this.playAttack(card, players, scores)
62+
} else if (type in this) {
63+
move = this[type](card, {hand, players, stacks, scores})
64+
}
65+
if (move) { return move }
66+
}
67+
}
68+
return undefined
69+
}
70+
71+
/**
72+
* Returns a sorted list of the cards in a players hand.
73+
* Sorts by lowest order value and then by highest card value.
74+
* Cards types that are not in the playOrder will move to back.
75+
*/
76+
sortHand (hand) {
77+
return hand.cards.sort((a, b) => {
78+
if (!(a.type in this.playOrder)) { return 1 }
79+
else if (!(b.type in this.playOrder)) { return -1 }
80+
81+
if (a.type === b.type) {
82+
return b.value - a.value
83+
}
84+
return this.playOrder[a.type] - this.playOrder[b.type]
85+
})
86+
}
87+
88+
/**
89+
* Make a move for an instruction card.
90+
* It is currently always possible to start a new instruction if a player
91+
* has an instruction card.
92+
* @param card The card to attempt to play.
93+
* @param state an object with all the state needed to make a decision
94+
* @return a move object for starting a new stack with the given card.
95+
*/
96+
instruction (card, state) { // eslint-disable-line no-unused-vars
97+
return {
98+
playType: 'startNewStack',
99+
card: card,
100+
player: this.player,
101+
target: this.player
102+
}
103+
}
104+
105+
/**
106+
* Make a move for adding a repeat card to the largest stack that
107+
* is available.
108+
* Should prioritize group stacks over normal stacks (even if lower in value?)
109+
* @param card The card to attempt to play.
110+
* @param state an object with all the state needed to make a decision
111+
* @return a move object for adding a repeat to a stack, or undefined if
112+
* no stack can be played on.
113+
*/
114+
repeat (card, state) {
115+
// get the player owned stack with the largest score
116+
let stack = state.stacks.filter((s) => {
117+
return s.playerId === this.player.id && s.willAccept(card)
118+
}).sort((a, b) => {
119+
return b.getScore() - a.getScore()
120+
}).shift()
121+
122+
if (stack) {
123+
return {
124+
playType: 'playCardOnStack',
125+
card: card,
126+
player: this.player,
127+
target: stack
128+
}
129+
}
130+
return undefined
131+
}
132+
133+
/**
134+
* Make a move for adding a variable card to the best stack available.
135+
* Prioritizes unmatched Rx cards, then stack with lowest variable in it.
136+
* @param card The card to attempt to play.
137+
* @param state an object with all the state needed to make a decision
138+
* @return a move object for adding a variable to a stack, or undefined if
139+
* no stack can be played on.
140+
*/
141+
variable (card, state) {
142+
let stacks = state.stacks.filter((s) => {
143+
return s.playerId === this.player.id && s.willAccept(card)
144+
})
145+
let stack = stacks.sort(helpers.varStackCompare).shift()
146+
147+
if (stack) {
148+
return {
149+
playType: 'playCardOnStack',
150+
card: card,
151+
player: this.player,
152+
target: stack
153+
}
154+
}
155+
return undefined
156+
}
157+
158+
/**
159+
* Hack another players stack under specific conditions.
160+
* Will not hack single card stacks as this is a waste. Picks the biggest
161+
* stack available that meets the criteria.
162+
* @param card The card to attempt to play.
163+
* @param state an object with all the state needed to make a decision
164+
* @return a move object for hacking a stack, or undefined if
165+
* no stack can be hacked.
166+
*/
167+
hack (card, state) {
168+
let stack = state.stacks.filter((s) => {
169+
return s.playerId !== this.player.id && s.cards.length > 1 && s.isHackable()
170+
}).sort((a, b) => {
171+
return b.getScore() - a.getScore()
172+
}).shift()
173+
174+
if (stack) {
175+
return {
176+
playType: 'hackStack',
177+
card: card,
178+
player: this.player,
179+
target: stack
180+
}
181+
}
182+
return undefined
183+
}
184+
185+
/**
186+
* Play a safety card on oneself if not already protected.
187+
* @param card The card to attempt to play.
188+
* @param state an object with all the state needed to make a decision
189+
* @return a move object for playing a safety, or undefined
190+
* if the player is already protected.
191+
*/
192+
playSafety (card) {
193+
if (!this.player.helpedBy(card.type)) {
194+
return {
195+
playType: 'playSpecialCard',
196+
card: card,
197+
player: this.player,
198+
target: this.player
199+
}
200+
}
201+
return undefined
202+
}
203+
204+
/**
205+
* Play an attack card on the opponent with the highest score that
206+
* is not already attacked by or protected from the card.
207+
* @param card The card to attempt to play.
208+
* @param state an object with all the state needed to make a decision
209+
* @return a move object for playing an attack, or undefined
210+
* no target can be found.
211+
*/
212+
playAttack (card, players, scores) {
213+
let target = players.filter((p) => {
214+
return p.id !== this.player.id && !p.hurtBy(card.type) && !p.isProtectedFrom(card.type)
215+
}).sort((a, b) => {
216+
return scores[b.id].score - scores[a.id].score
217+
}).shift()
218+
219+
if (target) {
220+
return {
221+
playType: 'playSpecialCard',
222+
card: card,
223+
player: this.player,
224+
target: target
225+
}
226+
}
227+
return undefined
228+
}
229+
230+
/**
231+
* Find the grouping of stacks that uses the most stacks if one exists.
232+
* @param card The card to attempt to play.
233+
* @param state an object with all the state needed to make a decision
234+
* @return a move object for grouping some stacks, or undefined if
235+
* no group can be found.
236+
*/
237+
group (card, state) {
238+
let groupable = state.stacks.filter((s) => {
239+
// don't group single group cards with the same value as card
240+
return s.playerId === this.player.id && s.getScore() <= card.value
241+
})
242+
if (groupable.length == 0) { return undefined }
243+
244+
let stacks = helpers.groupStacks(card.value, groupable)
245+
if (stacks.size > 0) {
246+
return {
247+
playType: 'groupStacks',
248+
card: card,
249+
player: this.player,
250+
target: stacks
251+
}
252+
}
253+
return undefined
254+
}
255+
}
256+

0 commit comments

Comments
 (0)