/
CleverBot.py
1168 lines (1097 loc) · 53 KB
/
CleverBot.py
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
__copyright__ = "Copyright (c) 2010-2012 David Ripton"
__license__ = "GNU GPL v2"
"""An attempt at a smarter AI."""
import random
import copy
import time
from sys import maxint
import itertools
import collections
import logging
from twisted.python import log
from zope.interface import implementer
from slugathon.ai.Bot import Bot
from slugathon.ai import BotParams
from slugathon.game import Game, Creature, Phase, Legion
def best7(score_moves):
"""Return a set of the the best (highest score) (up to) 7 moves from
score_moves, which is a sorted list of (score, move) tuples.
If there's a tie, pick at random.
"""
score_moves = score_moves[:]
best_moves = set()
while score_moves and len(best_moves) < 7:
choices = []
best_score = score_moves[-1][0]
for (score, move) in reversed(score_moves):
if score == best_score:
choices.append((score, move))
else:
break
(score, move) = random.choice(choices)
score_moves.remove((score, move))
best_moves.add(move)
return best_moves
@implementer(Bot)
class CleverBot(object):
def __init__(self, playername, ai_time_limit, bot_params=None):
logging.info("CleverBot %s %s", playername, ai_time_limit)
self.playername = playername
self.user = None
self.ai_time_limit = ai_time_limit
self.best_creature_moves = None
if bot_params is None:
self.bp = BotParams.default_bot_params
else:
self.bp = bot_params
@property
def player_info(self):
"""Return a string with information for result-tracking purposes."""
return str(self.bp)
def maybe_pick_color(self, game):
logging.info("maybe_pick_color")
if game.next_playername_to_pick_color == self.playername:
color = random.choice(game.colors_left)
def1 = self.user.callRemote("pick_color", game.name, color)
def1.addErrback(self.failure)
def maybe_pick_first_marker(self, game, playername):
logging.info("maybe_pick_first_marker")
if playername == self.playername:
player = game.get_player_by_name(playername)
markerid = self._choose_marker(player)
self._pick_marker(game, self.playername, markerid)
def _pick_marker(self, game, playername, markerid):
logging.info("pick_marker")
player = game.get_player_by_name(playername)
if markerid is None:
if not player.markerid_to_legion:
self.maybe_pick_first_marker(game, playername)
else:
player.pick_marker(markerid)
if not player.markerid_to_legion:
def1 = self.user.callRemote("pick_first_marker", game.name,
markerid)
def1.addErrback(self.failure)
def _choose_marker(self, player):
"""Pick a legion marker randomly, except prefer my own markers
to captured ones to be less annoying."""
own_markerids_left = [name for name in player.markerids_left if
name[:2] == player.color_abbrev]
if own_markerids_left:
return random.choice(own_markerids_left)
else:
return random.choice(list(player.markerids_left))
# TODO Avoid hitting 100-point multiples with 7-high legions.
def choose_engagement(self, game):
"""Resolve engagements.
Fight with legions that have angels first (so we can summon them),
then with the most important legion.
"""
logging.info("choose_engagement")
if (game.pending_summon or game.pending_reinforcement or
game.pending_acquire):
logging.info("choose_engagement bailing early %s %s %s",
game.pending_summon, game.pending_reinforcement,
game.pending_acquire)
return
hexlabels = list(game.engagement_hexlabels)
if hexlabels:
if len(hexlabels) >= 2:
player = game.get_player_by_name(self.playername)
value_to_hexlabel = []
for hexlabel in hexlabels:
legion = player.friendly_legions(hexlabel=hexlabel).pop()
value = legion.sort_value
if not player.summoned and legion.any_summonable:
value += 1000
value_to_hexlabel.append((value, hexlabel))
value_to_hexlabel.sort()
(value, hexlabel) = value_to_hexlabel[-1]
else:
hexlabel = hexlabels[0]
logging.info("calling resolve_engagement")
def1 = self.user.callRemote("resolve_engagement", game.name,
hexlabel)
def1.addErrback(self.failure)
else:
logging.info("CleverBot calling done_with_engagements")
def1 = self.user.callRemote("done_with_engagements", game.name)
def1.addErrback(self.failure)
# TODO concede, negotiate
def resolve_engagement(self, game, hexlabel):
"""Resolve the engagement in hexlabel."""
logging.info("resolve_engagement %s %s", game, hexlabel)
attacker = None
defender = None
for legion in game.all_legions(hexlabel):
if legion.player == game.active_player:
attacker = legion
else:
defender = legion
if attacker:
logging.info("attacker %s %s", attacker, attacker.player.name)
if defender:
logging.info("defender %s %s", defender, defender.player.name)
if not attacker or not defender:
logging.info("no attacker or defender; bailing")
return
if defender.player.name == self.playername:
if game.defender_chose_not_to_flee:
logging.info("defender already chose not to flee")
else:
logging.info("defender hasn't chosen whether to flee yet")
if defender.can_flee:
logging.info("can flee")
if (defender.terrain_combat_value * self.bp.FLEE_RATIO <
attacker.terrain_combat_value):
logging.info("fleeing")
def1 = self.user.callRemote("flee", game.name,
defender.markerid)
def1.addErrback(self.failure)
else:
logging.info("not fleeing")
def1 = self.user.callRemote("do_not_flee", game.name,
defender.markerid)
def1.addErrback(self.failure)
else:
logging.info("can't flee")
def1 = self.user.callRemote("do_not_flee", game.name,
defender.markerid)
def1.addErrback(self.failure)
elif attacker.player.name == self.playername:
if not game.defender_chose_not_to_flee:
# Wait for the defender to choose whether to flee.
logging.info("waiting for defender")
else:
logging.info("attacker fighting")
def1 = self.user.callRemote("fight", game.name,
attacker.markerid, defender.markerid)
def1.addErrback(self.failure)
else:
logging.info("not my engagement")
# TODO Look at what we can recruit on each movement roll, to see if we
# should take a third creature of a kind. We also need to be able to
# split off better creatures to enable recruiting for this to be
# useful for 6-high legions.
def _pick_recruit_and_recruiters(self, legion):
"""Return a tuple of (recruit, recruiters), or (None, None)."""
player = legion.player
game = player.game
caretaker = game.caretaker
masterhex = game.board.hexes[legion.hexlabel]
mterrain = masterhex.terrain
lst = legion.available_recruits_and_recruiters(mterrain,
caretaker)
if lst:
# For now, just take the last recruit.
tup = lst[-1]
recruit = tup[0]
# And pick randomly among its recruiters.
possible_recruiters = set()
for tup in lst:
if tup[0] == recruit:
recruiters = tup[1:]
possible_recruiters.add(recruiters)
recruiters = random.choice(list(possible_recruiters))
return (recruit, recruiters)
return (None, None)
def recruit(self, game):
logging.info("CleverBot.recruit")
if game.active_player.name != self.playername:
logging.info("not my turn")
return
player = game.active_player
for legion in player.legions:
if legion.moved and legion.can_recruit:
recruit, recruiters = self._pick_recruit_and_recruiters(legion)
if recruit is not None:
logging.info("CleverBot calling recruit_creature %s %s %s",
legion.markerid, recruit, recruiters)
def1 = self.user.callRemote("recruit_creature", game.name,
legion.markerid, recruit, recruiters)
def1.addErrback(self.failure)
return
logging.info("CleverBot calling done_with_recruits")
def1 = self.user.callRemote("done_with_recruits", game.name)
def1.addErrback(self.failure)
def reinforce(self, game):
"""Reinforce, during the REINFORCE battle phase or after the battle."""
logging.info("")
during_battle = (game.battle_phase == Phase.REINFORCE)
if during_battle:
assert game.battle_active_player.name == self.playername
legion = game.defender_legion
assert legion.player.name == self.playername
if (not during_battle or game.battle_turn == 4) and legion.can_recruit:
recruit, recruiters = self._pick_recruit_and_recruiters(legion)
if recruit is not None:
logging.info("CleverBot calling recruit_creature %s", recruit)
def1 = self.user.callRemote("recruit_creature", game.name,
legion.markerid, recruit, recruiters)
def1.addErrback(self.failure)
return
if during_battle:
logging.info("CleverBot calling done_with_reinforcements")
def1 = self.user.callRemote("done_with_reinforcements", game.name)
def1.addErrback(self.failure)
else:
logging.info("CleverBot calling do_not_reinforce %s", recruit)
def1 = self.user.callRemote("do_not_reinforce", game.name,
legion.markerid)
def1.addErrback(self.failure)
# TODO Consider value of this legion and donor legion before deciding
# whether to summon. But we may want to summon from a greater legion
# if it's 7 high and summoning lets it recruit.
def summon_angel(self, game):
logging.info("")
assert game.active_player.name == self.playername
during_battle = (game.battle_phase == Phase.REINFORCE)
legion = game.attacker_legion
assert legion.player.name == self.playername
if (legion.can_summon and (not during_battle or
game.first_attacker_kill in [game.battle_turn - 1,
game.battle_turn])):
summonables = legion.player.all_summonables
if summonables:
tuples = sorted(((creature.sort_value, creature)
for creature in summonables), reverse=True)
summonable = tuples[0][1]
# After battle, do not summon if 6 high and we could recruit
# something at least as good as the summonable.
recruit = None
if not during_battle:
if (len(legion) >= 6 and legion.can_recruit and
legion.moved):
recruit_name, _ = self._pick_recruit_and_recruiters(
legion)
if recruit_name:
recruit = Creature.Creature(recruit_name)
if not recruit or summonable.sort_value > recruit.sort_value:
donor = summonable.legion
logging.info(
"CleverBot calling _summon_angel %s %s %s",
legion.markerid, donor.markerid, summonable.name)
def1 = self.user.callRemote("summon_angel", game.name,
legion.markerid, donor.markerid, summonable.name)
def1.addErrback(self.failure)
return
logging.info("CleverBot calling do_not_summon_angel %s",
legion.markerid)
def1 = self.user.callRemote("do_not_summon_angel", game.name,
legion.markerid)
def1.addErrback(self.failure)
if during_battle:
logging.info("CleverBot calling done_with_reinforcements")
def1 = self.user.callRemote("done_with_reinforcements", game.name)
def1.addErrback(self.failure)
def acquire_angels(self, game, markerid, num_angels, num_archangels):
logging.info("CleverBot.acquire_angels %s %s %s", markerid, num_angels,
num_archangels)
player = game.get_player_by_name(self.playername)
legion = player.markerid_to_legion.get(markerid)
if legion is None:
return
starting_height = len(legion)
acquires = 0
angel_names = []
while starting_height + acquires < 7 and num_archangels:
angel_names.append("Archangel")
num_archangels -= 1
acquires += 1
while starting_height + acquires < 7 and num_angels:
angel_names.append("Angel")
num_angels -= 1
acquires += 1
if angel_names:
# If we can recruit something better than the worst angel, don't
# take it.
if (len(legion) + acquires >= 7 and legion.can_recruit and
legion.moved):
recruit_name, _ = self._pick_recruit_and_recruiters(legion)
if recruit_name:
recruit = Creature.Creature(recruit_name)
angel = Creature.Creature(angel_names[-1])
if recruit.sort_value > angel.sort_value:
angel_names = angel_names[:-1]
if angel_names:
logging.info("CleverBot calling acquire_angels %s %s",
markerid, angel_names)
def1 = self.user.callRemote("acquire_angels", game.name,
markerid, angel_names)
def1.addErrback(self.failure)
return
logging.info("CleverBot calling do_not_acquire_angels %s",
markerid)
def1 = self.user.callRemote("do_not_acquire_angels", game.name,
markerid)
def1.addErrback(self.failure)
# TODO Sometimes split off a better creature to enable better recruiting.
# TODO Fear being caught from behind by a bigger legion.
def split(self, game):
"""Split a legion, or end split phase."""
logging.info("split")
logging.info("playername %s active_playername %s", self.playername,
game.active_player.name)
if game.active_player.name != self.playername:
logging.warning("called split out of turn; exiting")
return
player = game.active_player
caretaker = game.caretaker
# Let more important legions get the first crack at markers.
for legion in player.sorted_legions:
if len(legion) == 8:
if game.turn != 1:
raise AssertionError("8-high legion", legion)
# initial split 4-4, one lord per legion
# TODO Parameterize and train initial split preferences
# split_gargs, split_ogres, split_centaurs,
# gargs_with_titan, ogres_with_titan, centaurs_with_titan
logging.info("initial split")
new_markerid = self._choose_marker(player)
lord = random.choice(["Titan", "Angel"])
creatures = ["Centaur", "Gargoyle", "Ogre"]
creature1 = random.choice(creatures)
creature2 = creature1
creature3 = creature1
while creature3 == creature1:
creature3 = random.choice(creatures)
new_creatures = [lord, creature1, creature2, creature3]
old_creatures = legion.creature_names
for creature in new_creatures:
old_creatures.remove(creature)
logging.info("new_creatures %s old_creatures %s",
new_creatures, old_creatures)
def1 = self.user.callRemote("split_legion", game.name,
legion.markerid, new_markerid, old_creatures, new_creatures)
def1.addErrback(self.failure)
return
elif len(legion) == 7 and player.markerids_left:
logging.info("7-high")
good_recruit_rolls = set()
safe_split_rolls = set()
new_markerid = self._choose_marker(player)
lst = legion.sorted_creatures
split = lst[-2:]
split_names = [creature.name for creature in split]
keep = lst[:-2]
keep_names = [creature.name for creature in keep]
# Pretend to split so that we can compute the bigger
# child legion's combat value.
new_legion1 = Legion.Legion(legion.player, legion.markerid,
Creature.n2c(keep_names), legion.hexlabel)
for roll in xrange(1, 6 + 1):
moves = game.find_all_moves(legion,
game.board.hexes[legion.hexlabel], roll)
for hexlabel, entry_side in moves:
masterhex = game.board.hexes[hexlabel]
terrain = masterhex.terrain
recruits = legion.available_recruits(terrain,
caretaker)
if recruits:
recruit = Creature.Creature(recruits[-1])
if recruit.sort_value > lst[-1].sort_value:
good_recruit_rolls.add(roll)
enemies = player.enemy_legions(hexlabel)
if enemies:
enemy = enemies.pop()
new_legion1.hexlabel = hexlabel
if (enemy.terrain_combat_value < self.bp.SQUASH *
new_legion1.terrain_combat_value):
safe_split_rolls.add(roll)
else:
safe_split_rolls.add(roll)
if good_recruit_rolls and len(safe_split_rolls) == 6:
def1 = self.user.callRemote("split_legion", game.name,
legion.markerid, new_markerid, keep_names, split_names)
def1.addErrback(self.failure)
return
# No splits, so move on to the next phase.
def1 = self.user.callRemote("done_with_splits", game.name)
def1.addErrback(self.failure)
def move_legions(self, game):
"""Move one or more legions, and then end the Move phase."""
logging.debug("")
if game.active_player.name != self.playername:
logging.info("not active player; aborting")
return
player = game.active_player
non_moves = {} # markerid: score
while True:
# Score moves
# (score, legion, hexlabel, entry_side)
best_moves = []
for legion in player.unmoved_legions:
moves = game.find_all_moves(legion, game.board.hexes[
legion.hexlabel], player.movement_roll)
logging.debug("legion %s moves %s", legion, moves)
for hexlabel, entry_side in moves:
score = self._score_move(legion, hexlabel, True)
best_moves.append(
(score, legion, hexlabel, entry_side))
best_moves.sort()
logging.debug("best moves %s", best_moves)
if player.can_take_mulligan:
legions_with_good_moves = set()
for (score, legion, _, _) in best_moves:
if score > 0:
legions_with_good_moves.add(legion)
if len(legions_with_good_moves) < 2:
logging.info("taking a mulligan")
def1 = self.user.callRemote("take_mulligan", game.name)
def1.addErrback(self.failure)
return
# Score non-moves
# (score, legion, hexlabel, None)
# Entry side None means not a move.
for legion in player.unmoved_legions:
score = self._score_move(legion, legion.hexlabel, False)
non_moves[legion.markerid] = score
logging.debug("non_moves %s", non_moves)
while best_moves:
score, legion, hexlabel, entry_side = best_moves.pop()
non_move_score = non_moves[legion.markerid]
if (score > non_move_score or not player.moved_legions or
len(player.friendly_legions(legion.hexlabel)) >= 2):
if entry_side == Game.TELEPORT:
teleport = True
masterhex = game.board.hexes[hexlabel]
terrain = masterhex.terrain
if terrain == "Tower":
entry_side = 5
else:
entry_side = random.choice([1, 3, 5])
teleporting_lord = sorted(legion.lord_types)[-1]
else:
teleport = False
teleporting_lord = None
def1 = self.user.callRemote("move_legion", game.name,
legion.markerid, hexlabel, entry_side, teleport,
teleporting_lord)
def1.addErrback(self.failure)
return
# No more legions will move.
logging.debug("done_with_moves")
def1 = self.user.callRemote("done_with_moves", game.name)
def1.addErrback(self.failure)
return
def _score_move(self, legion, hexlabel, moved):
"""Return a score for legion moving to (or staying in) hexlabel."""
score = 0
player = legion.player
game = player.game
caretaker = game.caretaker
board = game.board
enemies = player.enemy_legions(hexlabel)
legion_combat_value = legion.combat_value
legion_sort_value = legion.sort_value
if enemies:
assert len(enemies) == 1
enemy = enemies.pop()
enemy_combat_value = enemy.terrain_combat_value
logging.debug("legion %s hexlabel %s", legion, hexlabel)
logging.debug("legion_combat_value %s", legion_combat_value)
logging.debug("enemy_combat_value %s", enemy_combat_value)
if enemy_combat_value < self.bp.SQUASH * legion_combat_value:
score += enemy.score
elif (enemy_combat_value >= self.bp.BE_SQUASHED *
legion_combat_value):
score -= legion_sort_value
if moved and (len(legion) < 7 or enemies):
masterhex = board.hexes[hexlabel]
terrain = masterhex.terrain
recruits = legion.available_recruits(terrain, caretaker)
if recruits:
recruit_name = recruits[-1]
recruit = Creature.Creature(recruit_name)
# Only give credit for recruiting if we're likely to live.
if not enemies or enemy_combat_value < legion_combat_value:
recruit_value = recruit.sort_value
elif (enemy_combat_value < self.bp.BE_SQUASHED *
legion_combat_value):
recruit_value = 0.5 * recruit.sort_value
logging.debug("recruit value %s %s %s", legion.markerid,
hexlabel, recruit_value)
score += recruit_value
if game.turn > 1:
# Do not fear enemy legions on turn 1. 8-high legions will be
# forced to split, and hanging around in the tower to avoid getting
# attacked 5-on-4 is too passive.
try:
previous_hexlabel = legion.hexlabel
legion.hexlabel = hexlabel
for enemy in player.enemy_legions():
if (enemy.terrain_combat_value >= self.bp.BE_SQUASHED *
legion_combat_value):
for roll in xrange(1, 6 + 1):
moves = game.find_normal_moves(enemy,
game.board.hexes[enemy.hexlabel], roll).union(
game.find_titan_teleport_moves(enemy))
hexlabels = set((move[0] for move in moves))
if hexlabel in hexlabels:
score -= legion_sort_value / 6.0
finally:
legion.hexlabel = previous_hexlabel
return score
def _gen_legion_moves_inner(self, movesets):
"""Yield tuples of distinct hexlabels, one from each moveset, in order,
with no duplicates.
movesets is a list of sets of hexlabels, corresponding to the order of
remaining creatures in the legion.
"""
if not movesets:
yield ()
elif len(movesets) == 1:
for move in movesets[0]:
yield (move,)
else:
for move in movesets[0]:
movesets1 = copy.deepcopy(movesets[1:])
for moveset in movesets1:
moveset.discard(move)
for moves in self._gen_legion_moves_inner(movesets1):
yield (move,) + moves
def _gen_legion_moves(self, movesets):
"""Yield all possible legion_moves for movesets.
movesets is a list of sets of hexlabels to which each Creature can move
(or stay), in the same order as Legion.sorted_creatures. Like:
creatures [titan1, ogre1, troll1]
movesets [{"A1", "A2", "B1"}, {"B1", "B2"}, {"B1", "B3"}]
A legion_move is a list of hexlabels, in the same order as creatures,
where each Creature's hexlabel is one from its original list, and no
two Creatures have the same hexlabel. Like:
["A1", "B1", "B3"]
"""
logging.info("_gen_legion_moves %s", movesets)
for moves in self._gen_legion_moves_inner(movesets):
if len(moves) == len(movesets):
yield list(moves)
def _gen_fallback_legion_moves(self, movesets):
"""Yield all possible legion_moves for movesets, possibly including
some missing moves in the case where not all creatures can get onboard.
movesets is a list of sets of hexlabels to which each Creature can move
(or stay), in the same order as Legion.sorted_creatures. Like:
creatures [titan1, ogre1, troll1]
movesets [{"A1", "A2", "B1"}, {"B1", "B2"}, {"B1", "B3"}]
A legion_move is a list of hexlabels, in the same order as creatures,
where each Creature's hexlabel is one from its original list, and no
two Creatures have the same hexlabel. Like:
["A1", "B1", "B3"]
"""
logging.info(movesets)
for moves in self._gen_legion_moves_inner(movesets):
yield list(moves)
def _score_perm(self, game, sort_values, perm):
"""Score one move order permutation."""
score = 0
moved_creatures = set()
try:
for creature_name, start, move in perm:
creature = game.creatures_in_battle_hex(start,
creature_name).pop()
if (move == start or move in
game.find_battle_moves(creature)):
creature.previous_hexlabel = creature.hexlabel
creature.hexlabel = move
moved_creatures.add(creature)
score += sort_values[creature_name]
return score
finally:
for creature in moved_creatures:
creature.hexlabel = creature.previous_hexlabel
creature.previous_hexlabel = None
def _find_move_order(self, game, creature_moves):
"""Return a new list with creature_moves rearranged so that as
many of the moves as possible can be legally made.
creature_moves is a list of (creature_name, start_hexlabel,
finish_hexlabel) tuples.
"""
max_score = 0
sort_values = {}
for creature_name, start, move in creature_moves:
creature = game.creatures_in_battle_hex(start, creature_name).pop()
sort_values[creature_name] = creature.sort_value
max_score += creature.sort_value
perms = list(itertools.permutations(creature_moves))
logging.info("%d perms" % len(perms))
# Scramble the list so we don't get a bunch of similar bad
# orders jumbled together at the beginning.
random.shuffle(perms)
best_score = -maxint
best_perm = None
finish_time = time.time() + self.ai_time_limit
for perm in perms:
score = self._score_perm(game, sort_values, perm)
if score == max_score:
best_perm = perm
logging.info("found perfect order")
break
elif score > best_score:
best_perm = perm
best_score = score
if time.time() > finish_time:
logging.info("time limit")
break
logging.info("returning %s" % list(best_perm))
return list(best_perm)
def _find_best_creature_moves(self, game):
"""Return a list of up to one (creature_name, start_hexlabel,
finish_hexlabel) tuple for each Creature in the battle active legion.
Idea: Find all possible moves for each creature in the legion,
ignoring mobile allies, and score them in isolation without knowing
where its allies will end up. Find the best 7 moves for each creature
(because with up to 7 creatures in a legion, a creature may have to
take its 7th-favorite move, and because 7! = 5040, not too big).
Combine these into legion moves, and score those again, then take
the best legion move. Finally, find the order of creature moves
that lets all the creatures reach their assigned hexes without
blocking their allies' moves.
"""
if (game.battle_active_player is None or game.battle_active_player.name
!= self.playername):
return None
legion = game.battle_active_legion
creatures = legion.sorted_living_creatures
logging.info("_find_best_creature_moves %s %s", legion, creatures)
if not creatures:
return None
movesets = [] # list of a set of hexlabels for each creature
previous_creature = None
moveset = None
for creature in creatures:
if (previous_creature and creature.name == previous_creature.name
and creature.hexlabel == previous_creature.hexlabel):
# Reuse previous moveset
moveset = copy.deepcopy(moveset)
else:
moves = game.find_battle_moves(creature,
ignore_mobile_allies=True)
if moves:
score_moves = []
# Not moving is also an option, unless offboard.
if creature.hexlabel not in ["ATTACKER", "DEFENDER"]:
moves.add(creature.hexlabel)
for move in moves:
try:
creature.previous_hexlabel = creature.hexlabel
creature.hexlabel = move
score = self._score_legion_move(game, [creature])
score_moves.append((score, move))
finally:
creature.hexlabel = creature.previous_hexlabel
score_moves.sort()
logging.info("score_moves %s %s", creature, score_moves)
moveset = best7(score_moves)
else:
moveset = set([creature.hexlabel])
movesets.append(moveset)
previous_creature = creature
best_legion_move = None
now = time.time()
legion_moves = list(self._gen_legion_moves(movesets))
logging.info("found %d legion_moves in %fs" % (len(legion_moves),
time.time() - now))
if not legion_moves:
legion_moves = list(self._gen_fallback_legion_moves(movesets))
if not legion_moves:
return None
best_score = -maxint
start_time = time.time()
finish_time = start_time + self.ai_time_limit
# Scramble the moves, in case we don't have time to look at them all.
random.shuffle(legion_moves)
logging.info("len(creatures) %d len(legion_moves[0]) %d" % (
len(creatures), len(legion_moves[0])))
for legion_move in legion_moves:
try:
for ii, creature in enumerate(creatures):
move = legion_move[ii]
creature.previous_hexlabel = creature.hexlabel
creature.hexlabel = move
score = self._score_legion_move(game, creatures)
if score > best_score:
best_legion_move = legion_move
best_score = score
now = time.time()
if now > finish_time:
logging.info("time limit")
break
finally:
for creature in creatures:
creature.hexlabel = creature.previous_hexlabel
logging.info("found best_legion_move %s in %fs" % (best_legion_move,
now - start_time))
start_hexlabels = [creature.hexlabel for creature in creatures]
creature_names = [creature.name for creature in creatures]
creature_moves = zip(creature_names, start_hexlabels, best_legion_move)
logging.info("creature_moves %s", creature_moves)
now = time.time()
ordered_creature_moves = self._find_move_order(game, creature_moves)
logging.info("found ordered_creature_moves %s in %fs" % (
ordered_creature_moves, time.time() - now))
return ordered_creature_moves
def move_creatures(self, game):
"""Move all creatures in the legion.
Idea: Find all possible moves for each creature in the legion,
ignoring mobile allies, and score them in isolation without knowing
where its allies will end up. Find the best 7 moves for each creature
(because with up to 7 creatures in a legion, a creature may have to
take its 7th-favorite move, and because 7 ** 7 = 823543, not too big).
Combine these into legion moves, and score those again, then take
the best legion move. Finally, find the order of creature moves
that lets all the creatures reach their assigned hexes without
blocking their allies' moves.
"""
logging.info("CleverBot.move_creatures")
if self.best_creature_moves is None:
self.best_creature_moves = self._find_best_creature_moves(game)
# Loop in case a non-move is best.
while self.best_creature_moves:
(creature_name, start, finish) = \
self.best_creature_moves.pop(0)
logging.info("checking move %s %s %s", creature_name, start,
finish)
creatures = game.creatures_in_battle_hex(start, creature_name)
if creatures:
creature = creatures.pop()
else:
logging.info("best_creature_moves was broken")
self.best_creature_moves = self._find_best_creature_moves(game)
continue
if finish != start and finish in game.find_battle_moves(
creature):
logging.info("calling move_creature %s %s %s", creature.name,
start, finish)
def1 = self.user.callRemote("move_creature", game.name,
creature.name, start, finish)
def1.addErrback(self.failure)
return
# No moves, so end the maneuver phase.
logging.info("calling done_with_maneuvers")
self.best_creature_moves = None
def1 = self.user.callRemote("done_with_maneuvers", game.name)
def1.addErrback(self.failure)
def _score_legion_move(self, game, creatures):
"""Return a score for creatures in their current hexlabels."""
score = 0
battlemap = game.battlemap
legion = creatures[0].legion
legion2 = game.other_battle_legion(legion)
# For each enemy, figure out the average damage we could do to it if
# everyone concentrated on hitting it, and if that's enough to kill it,
# give every creature a kill bonus.
# (This is not quite right because each creature can only hit one enemy
# (ignoring carries), but it's a start.)
kill_bonus = 0
for enemy in legion2.creatures:
total_mean_hits = 0
for creature in creatures:
if (enemy in creature.engaged_enemies or
(not creature.engaged and enemy in
creature.rangestrike_targets)):
dice = creature.number_of_dice(enemy)
strike_number = creature.strike_number(enemy)
mean_hits = dice * (7. - strike_number) / 6
total_mean_hits += mean_hits
if total_mean_hits >= enemy.hits_left:
kill_bonus += enemy.sort_value
for creature in creatures:
can_rangestrike = False
engaged = creature.engaged_enemies
max_mean_hits = 0.0
total_mean_damage_taken = 0.0
engaged_with_rangestriker = False
# melee
for enemy in engaged:
# Damage we can do.
dice = creature.number_of_dice(enemy)
strike_number = creature.strike_number(enemy)
mean_hits = dice * (7. - strike_number) / 6
max_mean_hits = max(mean_hits, max_mean_hits)
# Damage we can take.
dice = enemy.number_of_dice(creature)
strike_number = enemy.strike_number(creature)
mean_hits = dice * (7. - strike_number) / 6
total_mean_damage_taken += mean_hits
if enemy.rangestrikes:
engaged_with_rangestriker = True
# inbound rangestriking
for enemy in legion2.creatures:
if enemy not in engaged:
if creature in enemy.potential_rangestrike_targets:
dice = enemy.number_of_dice(creature)
strike_number = enemy.strike_number(creature)
mean_hits = dice * (7. - strike_number) / 6
total_mean_damage_taken += mean_hits
probable_death = total_mean_damage_taken >= creature.hits_left
if engaged_with_rangestriker and not creature.rangestrikes:
score += self.bp.ENGAGE_RANGESTRIKER_BONUS
logging.info(creature, "ENGAGE_RANGESTRIKER_BONUS %s",
self.bp.ENGAGE_RANGESTRIKER_BONUS)
# rangestriking
if not engaged:
targets = creature.rangestrike_targets
for enemy in targets:
# Damage we can do
dice = creature.number_of_dice(enemy)
strike_number = creature.strike_number(enemy)
mean_hits = dice * (7. - strike_number) / 6
max_mean_hits = max(mean_hits, max_mean_hits)
can_rangestrike = True
if can_rangestrike:
score += self.bp.RANGESTRIKE_BONUS
logging.info(creature, "RANGESTRIKE_BONUS %s",
self.bp.RANGESTRIKE_BONUS)
# Don't encourage titans to charge early.
if (creature.name != "Titan" or game.battle_turn >= 4 or
len(legion) == 1):
if max_mean_hits:
bonus = self.bp.HIT_BONUS * max_mean_hits
score += bonus
logging.info(creature, "HIT_BONUS %s", bonus)
if kill_bonus:
bonus = self.bp.KILL_MULTIPLIER * kill_bonus
score += bonus
logging.info(creature, "KILL_BONUS %s", bonus)
if total_mean_damage_taken:
penalty = self.bp.DAMAGE_PENALTY * total_mean_damage_taken
score += penalty
logging.info(creature, "DAMAGE_PENALTY %s", penalty)
if probable_death:
penalty = (self.bp.DEATH_MULTIPLIER * probable_death *
creature.sort_value)
score += penalty
logging.info(creature, "DEATH_PENALTY %s", penalty)
# Attacker must attack to avoid time loss
# Don't encourage titans to charge early.
if legion == game.attacker_legion and (creature.name != "Titan"
or game.battle_turn >= 4 or len(legion) == 1):
if engaged or targets:
score += self.bp.ATTACKER_AGGRESSION_BONUS
logging.info(creature, "ATTACKER_AGGRESSION_BONUS %s",
self.bp.ATTACKER_AGGRESSION_BONUS)
else:
enemy_hexlabels = [enemy.hexlabel for enemy in
legion2.living_creatures]
if enemy_hexlabels:
min_range = min((battlemap.range(creature.hexlabel,
enemy_hexlabel) for enemy_hexlabel in
enemy_hexlabels))
penalty = min_range * self.bp.ATTACKER_DISTANCE_PENALTY
score += penalty
logging.info(creature, "ATTACKER_DISTANCE_PENALTY %s",
penalty)
battlehex = battlemap.hexes[creature.hexlabel]
terrain = battlehex.terrain
# Make titans hang back early.
if (creature.is_titan and game.battle_turn < 4 and
terrain != "Tower"):
if legion == game.attacker_legion:
entrance = "ATTACKER"
else:
entrance = "DEFENDER"
distance = battlemap.range(creature.hexlabel, entrance,
allow_entrance=True) - 2
penalty = distance * self.bp.TITAN_FORWARD_PENALTY
if penalty:
score += penalty
logging.info(creature, "TITAN_FORWARD_PENALTY %s", penalty)
# Make defenders hang back early.
if (legion == game.defender_legion and game.battle_turn < 4 and
terrain != "Tower"):
entrance = "DEFENDER"
distance = battlemap.range(creature.hexlabel, entrance,
allow_entrance=True) - 2
penalty = distance * self.bp.DEFENDER_FORWARD_PENALTY
if penalty:
score += penalty
logging.info(creature, "DEFENDER_FORWARD_PENALTY %s",
penalty)
# terrain
if battlehex.elevation:
bonus = battlehex.elevation * self.bp.ELEVATION_BONUS
score += bonus
logging.info(creature, "ELEVATION_BONUS %s", bonus)
if terrain == "Bramble":
if creature.is_native(terrain):
score += self.bp.NATIVE_BRAMBLE_BONUS
logging.info(creature, "NATIVE_BRAMBLE_BONUS %s",
self.bp.NATIVE_BRAMBLE_BONUS)
else:
score += self.bp.NON_NATIVE_BRAMBLE_PENALTY
logging.info(creature, "NON_NATIVE_BRAMBLE_PENALTY %s",
self.bp.NON_NATIVE_BRAMBLE_PENALTY)
elif terrain == "Tower":
# XXX Hardcoded to default Tower map
logging.info("%s TOWER_BONUS", creature)
score += self.bp.TOWER_BONUS
if battlehex.elevation == 2:
if creature.is_titan:
score += self.bp.TITAN_IN_CENTER_OF_TOWER_BONUS
logging.info("%s TITAN_IN_CENTER_OF_TOWER_BONUS %s",
creature, self.bp.TITAN_IN_CENTER_OF_TOWER_BONUS)
else:
score += self.bp.CENTER_OF_TOWER_BONUS
logging.info("%s CENTER_OF_TOWER_BONUS %s",
creature, self.bp.CENTER_OF_TOWER_BONUS)
elif (legion == game.defender_legion and