-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathagent_bee_v4_0_4_2.py
executable file
·1462 lines (1179 loc) · 47 KB
/
agent_bee_v4_0_4_2.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
# -*- coding: utf-8 -*-
"""
v4_0_4_2 <- v4_0_4_1
Optimize performance.
* cached nearest shipyards helps
Tournament - ID: 07SrD7, Name: Your Halite 4 Trueskill Ladder | Dimension - ID: h9TnUa, Name: Halite 4 Dimension
Status: running | Competitors: 6 | Rank System: trueskill
Total Matches: 283 | Matches Queued: 52
bee v4.0.4.2 | Rt1ftky0I1Kz | 28.9336081 | μ=31.189, σ=0.752 | 176
bee v4.0.1 | Vrn6cwYYM13i | 27.5890498 | μ=29.767, σ=0.726 | 168
tom v1.0.0 | PbucP5yQqqQz | 25.8789352 | μ=28.010, σ=0.710 | 157
bee v1.8 | m1m8EkthKdRV | 22.0219620 | μ=24.103, σ=0.694 | 191
optimus_mining | MGxeIHCBlkMo | 17.5503365 | μ=19.704, σ=0.718 | 220
c40 | m0zNXgIYW0HK | 16.5505709 | μ=18.742, σ=0.731 | 220
Tournament - ID: 2o1Fhp, Name: Your Halite 4 Trueskill Ladder | Dimension - ID: hWJvuc, Name: Halite 4 Dimension
Status: running | Competitors: 6 | Rank System: trueskill
Total Matches: 166 | Matches Queued: 49
bee v4.0.4.2 | RU4zmeR95EaN | 28.6239562 | μ=30.988, σ=0.788 | 101
bee v4.0.1 | QTyorhkIUoA1 | 26.6873190 | μ=29.018, σ=0.777 | 87
tom v1.0.0 | c80KmnnxI5ZW | 26.4088266 | μ=28.622, σ=0.738 | 112
bee v1.8 | 0EpnfYtUiktn | 23.7842152 | μ=25.967, σ=0.728 | 103
optimus_mining | UHtjRY0o6Kc0 | 16.7882363 | μ=19.021, σ=0.744 | 129
c40 | grSYWgqbiIyY | 16.7057715 | μ=18.963, σ=0.752 | 132
"""
import random
import timeit
import logging
from collections import Counter
from enum import Enum, auto
import networkx as nx
import numpy as np
import scipy.optimize
from kaggle_environments.envs.halite.helpers import *
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Mute print.
def print(*args, **kwargs):
pass
MIN_WEIGHT = -99999
BEGINNING_PHRASE_END_STEP = 60
NEAR_ENDING_PHRASE_STEP = 340
# If my halite is less than this, do not build ship or shipyard anymore.
MIN_HALITE_TO_BUILD_SHIPYARD = 1000
MIN_HALITE_TO_BUILD_SHIP = 1000
# Controls the number of ships.
MAX_SHIP_NUM = 30
# Threshold for attack enemy nearby my shipyard
TIGHT_ENEMY_SHIP_DEFEND_DIST = 5
LOOSE_ENEMY_SHIP_DEFEND_DIST = 7
AVOID_COLLIDE_RATIO = 0.95
# Threshod used to send bomb to enemy shipyard
POSSIBLE_MOVES = [
Point(0, 0),
Point(0, 1),
Point(0, -1),
Point(1, 0),
Point(-1, 0)
]
TURNS_OPTIMAL = np.array(
[[0, 2, 3, 4, 4, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 8],
[0, 1, 2, 3, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 7, 7, 7],
[0, 0, 2, 2, 3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 7],
[0, 0, 1, 2, 2, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6],
[0, 0, 0, 1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 6],
[0, 0, 0, 0, 0, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
# cached values
HALITE_RETENSION_BY_DIST = []
HALITE_GROWTH_BY_DIST = []
MANHATTAN_DISTS = None
def get_quadrant(p: Point):
if p.x > 0 and p.y >= 0:
return 1
if p.x <= 0 and p.y > 0:
return 2
if p.x < 0 and p.y <= 0:
return 3
if p.x >= 0 and p.y < 0:
return 4
assert p == Point(0, 0), "not exist quadrant: %s %s" % (p.x, p.y)
return 0
def optimal_mining_steps(C, H, rt_travel):
# How many turns should we plan on mining?
# C=carried halite, H=halite in square, rt_travel=steps to square and back to shipyard
if C == 0:
ch = 0
elif H == 0:
ch = TURNS_OPTIMAL.shape[0] - 1 # ?
else:
ch = int(np.log(C / H) * 2.5 + 5.5)
ch = min(max(ch, 0), TURNS_OPTIMAL.shape[0] - 1)
# rt_travel = int(np.clip(rt_travel, 0, TURNS_OPTIMAL.shape[1] - 1))
rt_travel = int(min(max(rt_travel, 0), TURNS_OPTIMAL.shape[1] - 1 ))
return TURNS_OPTIMAL[ch, rt_travel]
class Timer:
def __init__(self, logging_text=None):
self._logging_text = logging_text
self._start = None
self._end = None
self._interval = None
def __enter__(self):
self._start = timeit.default_timer()
return self
def __exit__(self, *args):
self._end = timeit.default_timer()
self._interval = self._end - self._start
if self._logging_text is not None:
logger.info("Took %.3f seconds for %s", self._interval,
self._logging_text)
@property
def interval(self):
return self._interval
def cargo(player):
"""Computes the cargo value for a player."""
return sum([s.halite for s in player.ships], 0)
def axis_manhattan_dists(a: Point, b: Point, size):
def dist(x, y):
v = abs(x - y)
return min(v, size - v)
return dist(a.x, b.x), dist(a.y, b.y)
def manhattan_dist(a: Point, b: Point, size):
if MANHATTAN_DISTS:
return MANHATTAN_DISTS[a.x * size + a.y][b.x * size + b.y]
dist_x, dist_y = axis_manhattan_dists(a, b, size)
return dist_x + dist_y
def has_enemy_ship(cell, me):
if not cell.ship_id:
return False
ship = cell.ship
return ship.player_id != me.id
def direction_to_ship_action(position, next_position, board_size):
if position == next_position:
return None
if (position + Point(0, 1)) % board_size == next_position:
return ShipAction.NORTH
if (position + Point(1, 0)) % board_size == next_position:
return ShipAction.EAST
if (position + Point(0, -1)) % board_size == next_position:
return ShipAction.SOUTH
if (position + Point(-1, 0)) % board_size == next_position:
return ShipAction.WEST
assert False, '%s, %s' % (position, next_position)
def make_move(position, move, board_size):
return (position + move) % board_size
def get_neighbor_cells(cell, include_self=False):
neighbor_cells = [cell] if include_self else []
neighbor_cells.extend([cell.north, cell.south, cell.east, cell.west])
return neighbor_cells
def init_globals(board):
growth_factor = board.configuration.regen_rate + 1.0
retension_rate_rate = 1.0 - board.configuration.collect_rate
size = board.configuration.size
global HALITE_GROWTH_BY_DIST
if not HALITE_GROWTH_BY_DIST:
HALITE_GROWTH_BY_DIST = [growth_factor**d for d in range(size**2 + 1)]
global HALITE_RETENSION_BY_DIST
if not HALITE_RETENSION_BY_DIST:
HALITE_RETENSION_BY_DIST = [
retension_rate_rate**d for d in range(size**2 + 1)
]
global MANHATTAN_DISTS
dists = np.zeros((size**2, size**2), dtype=int)
with Timer("Init manhattan_dist"):
for c1 in board.cells.values():
for c2 in board.cells.values():
a = c1.position
b = c2.position
d = manhattan_dist(a, b, size)
dists[a.x * size + a.y][b.x * size + b.y] = d
MANHATTAN_DISTS = dists.tolist()
class ShipTask(Enum):
UNKNOWN_TASK = auto()
# Default task, to stay on current cell.
STAY = auto()
# Send a ship to a halite.
GOTO_HALITE = auto()
# Continuing collecting halite on current cell.
COLLECT = auto()
# Finish collecting halite and go back to shipyard.
RETURN = auto()
# Send a ship to attack enemy's shipyard.
ATTACK_SHIPYARD = auto()
# Attack enemy ship.
ATTACK_SHIP = auto()
# Send the first ship to a location to build the initial shipyard.
INITIAL_SHIPYARD = auto()
# Make one ship stay on the shipyard to protect it from enemy next to it.
GUARD_SHIPYARD = auto()
class StrategyBase:
"""Class with board related method."""
@property
def me(self):
return self.board.current_player
@property
def c(self):
return self.board.configuration
@property
def step(self):
return self.board.step
@property
def num_ships(self):
return len(self.me.ship_ids)
@property
def num_shipyards(self):
return len(self.me.shipyard_ids)
@property
def tight_defend_dist(self):
# return 4 + max((self.num_ships - 15) // 5, 0)
return TIGHT_ENEMY_SHIP_DEFEND_DIST
@property
def loose_defend_dist(self):
return LOOSE_ENEMY_SHIP_DEFEND_DIST
@property
def home_grown_cell_dist(self):
return self.tight_defend_dist
@property
def is_beginning_phrase(self):
return self.step <= BEGINNING_PHRASE_END_STEP
@property
def my_idle_ships(self):
"""All ships without task assignment."""
for ship in self.ships:
if ship.next_action or ship.has_assignment:
continue
yield ship
@property
def enemy_shipyards(self):
for e in self.board.opponents:
for y in e.shipyards:
yield y
@property
def enemy_ships(self):
for e in self.board.opponents:
for s in e.ships:
yield s
@staticmethod
def assign_task(ship, target_cell: Cell, task_type: ShipTask, enemy=None):
"""Add a task to a ship."""
ship.has_assignment = True
ship.target_cell = target_cell
ship.task_type = task_type
ship.target_cell.is_targetd = True
ship.target_enemy = enemy
def manhattan_dist(self, p, q):
return manhattan_dist(p.position, q.position, self.c.size)
def nearest_shipyards(self, cell: Cell, shipyards):
dist_yards = [(self.manhattan_dist(y, cell), y) for y in shipyards]
dist_yards = sorted(dist_yards, key=lambda x: x[0])
return dist_yards
def find_nearest_enemy(self, cell: Cell, enemy_ships):
"""Nearest enemy with least halite."""
if not isinstance(enemy_ships, list):
enemy_ships = list(enemy_ships)
enemy_ships = sorted(enemy_ships,
key=lambda s: (self.manhattan_dist(cell, s), s.halite))
for enemy in enemy_ships:
return self.manhattan_dist(cell, enemy), enemy
return 9999, None
def get_nearest_home_yard(self, cell):
if not hasattr(cell, 'home_yard_info'):
cell.nearest_home_yards = self.nearest_shipyards(cell, self.shipyards)
cell.home_yard_info = (9999, None)
if cell.nearest_home_yards:
cell.home_yard_info = cell.nearest_home_yards[0]
return cell.home_yard_info
def get_nearest_enemy_yard(self, cell):
if not hasattr(cell, 'enemy_yard_info'):
cell.nearest_enemy_yards = self.nearest_shipyards(cell, self.enemy_shipyards)
cell.enemy_yard_info = (9999, None)
if cell.nearest_enemy_yards:
cell.enemy_yard_info = cell.nearest_enemy_yards[0]
return cell.enemy_yard_info
def update(self, board):
self.board = board
# Cache it to eliminate repeated list constructor.
self.shipyards = self.me.shipyards
self.ships = self.me.ships
def execute(self):
pass
def __call__(self):
self.execute()
class FollowerDetector(StrategyBase):
# >= 2 is considered as following.
FOLLOW_COUNT = 2
def __init__(self):
self.board = None
self.ship_index = {} # Ship id => ship
self.follower = {} # ship_id => follower
self.follow_count = Counter()
def clear(self, ship_id):
if ship_id not in self.follower:
return
del self.follower[ship_id]
del self.follow_count[ship_id]
def add(self, ship_id, follower: Ship):
"""Note: follower.halite < ship.halite"""
prev_follower = self.follower.get(ship_id)
if prev_follower is None or prev_follower.id != follower.id:
# New follower.
self.follow_count[ship_id] = 1
else:
# Existing follower.
self.follow_count[ship_id] += 1
self.follower[ship_id] = follower
def update(self, board):
"""Updates follow info with the latest board state."""
super().update(board)
latest_ship_index = {s.id: s for s in self.ships}
# Check last ship positions for follower.
for ship_id, prev_ship in self.ship_index.items():
ship = latest_ship_index.get(ship_id)
if ship is None:
# The ship has gone.
self.clear(ship_id)
continue
follower = board[prev_ship.position].ship
if follower is None or follower.halite >= ship.halite:
# Not a follower.
self.clear(ship_id)
continue
assert follower and follower.halite < ship.halite
self.add(ship_id, follower)
# Update with latest ship position.
self.ship_index = latest_ship_index
def is_followed(self, ship: Ship):
"""Returns true if the ship of mine is traced by enemy."""
follower = self.follower.get(ship.id)
assert not follower or follower.halite < ship.halite
follow_count = self.follow_count.get(ship.id, 0)
return follow_count >= self.FOLLOW_COUNT
def get_follower(self, ship: Ship):
return self.follower.get(ship.id)
class InitializeFirstShipyard(StrategyBase):
def __init__(self):
super().__init__()
self.first_shipyard_set = False
self.initial_yard_position = None
self.initial_ship_position = None
def estimate_cell_halite(self, candidate_cell):
expected_halite = 0
current_halite = 0
num_halite_cells = 0
for cell in self.halite_cells:
# shipyard will destory the halite under it.
if candidate_cell.position == cell.position:
continue
dist = self.manhattan_dist(cell, candidate_cell)
# TODO(wangfei): try larger value?
if dist <= self.home_grown_cell_dist and cell.halite > 0:
expected_halite += self.halite_per_turn(None, cell, dist, dist)
current_halite += cell.halite
num_halite_cells += 1
return expected_halite, current_halite, dist
def select_initial_cell(self):
def get_coord_range(v):
DELTA = 0
MARGIN = 5
if v == 5:
v_min, v_max = MARGIN, 5 + DELTA
else:
v_min, v_max = 15 - DELTA, 20 - MARGIN
return v_min, v_max
position = self.initial_ship_position
x_min, x_max = get_coord_range(position.x)
y_min, y_max = get_coord_range(position.y)
for cell in self.board.cells.values():
position = cell.position
if (x_min <= position.x <= x_max and y_min <= position.y <= y_max):
yield self.estimate_cell_halite(cell), cell
def convert_first_shipyard(self):
"""Strategy for convert the first shipyard."""
assert self.num_ships == 1, self.num_ships
ship = self.ships[0]
if not self.initial_ship_position:
self.initial_ship_position = ship.position
candidate_cells = list(self.select_initial_cell())
if candidate_cells:
candidate_cells.sort(key=lambda x: x[0], reverse=True)
value, yard_cell = candidate_cells[0]
self.initial_yard_position = yard_cell.position
print(
"Ship initial:", self.initial_ship_position, 'dist=',
manhattan_dist(self.initial_ship_position,
self.initial_yard_position, self.c.size),
'selected yard position:', self.initial_yard_position, 'value=',
value)
self.assign_task(ship, self.board[self.initial_yard_position],
ShipTask.INITIAL_SHIPYARD)
if ship.position == self.initial_yard_position:
ship.next_action = ShipAction.CONVERT
self.first_shipyard_set = True
class ShipStrategy(InitializeFirstShipyard, StrategyBase):
"""Sends every ships to the nearest cell with halite.
cell:
|is_targetd|: prevent more than 1 alley ships choose the same halite.
ship:
|next_cell|: next cell location of the ship.
|has_assignment|: has already has task assignment for this ship.
|target_cell|: send ship to this cell, may be the same of current cell.
|task_type|: used to rank ship for moves.
"""
def __init__(self, simulation=False):
super().__init__()
self.board = None
self.simulation = simulation
self.follower_detector = FollowerDetector()
def update(self, board):
"""Updates board state at each step."""
if self.board is None:
init_globals(board)
super().update(board)
self.board = board
self.cost_halite = 0
self.halite_ratio = -1
self.num_home_halite_cells = 0
self.mean_home_halite = 100
self.init_halite_cells()
# Default ship to stay on the same cell without assignment.
for ship in self.ships:
ship.has_assignment = False
ship.target_cell = ship.cell
ship.next_cell = ship.cell
ship.task_type = ShipTask.STAY
self.follower_detector.update(board)
def init_halite_cells(self):
HOME_GROWN_CELL_MIN_HALITE = 80
def keep_halite_value(cell):
threshold = self.mean_halite_value * 0.7
if self.step >= NEAR_ENDING_PHRASE_STEP:
return min(40, threshold)
num_covered = len(cell.convering_shipyards)
if (num_covered >= 2
or num_covered > 0 and cell.convering_shipyards[0][0] <= 2):
keep = HOME_GROWN_CELL_MIN_HALITE
if num_covered >= 2:
keep *= 2
threshold = max(keep, threshold)
# Do not go into enemy shipyard for halite.
enemy_yard_dist, enemy_yard = self.get_nearest_enemy_yard(cell)
if (enemy_yard and enemy_yard_dist <= 5):
ally_yard_dist, alley_yard = self.get_nearest_home_yard(cell)
if (alley_yard and enemy_yard_dist < ally_yard_dist):
# if the cell is nearer to the enemy yard.
return 999
return threshold
# Init halite cells
self.halite_cells = []
for cell in self.board.cells.values():
cell.is_targetd = False
if cell.halite > 0:
self.halite_cells.append(cell)
# Initialize covered cells by shipyards.
for cell in self.halite_cells:
# Populate cache
self.get_nearest_home_yard(cell)
home_yards = [x for x in cell.nearest_home_yards
if x[0] <= self.home_grown_cell_dist]
cell.convering_shipyards = home_yards
self.mean_halite_value = 0
if self.halite_cells:
halite_values = [c.halite for c in self.halite_cells]
self.mean_halite_value = np.mean(halite_values)
self.std_halite_value = np.std(halite_values)
for cell in self.halite_cells:
cell.keep_halite_value = keep_halite_value(cell)
@property
def me_halite(self):
return self.me.halite - self.cost_halite
def collect_game_info(self):
# Computes neighbour cells mean halite values.
# TODO: reuse
def cell_to_yard_dist(cell):
min_dist, _ = self.get_nearest_home_yard(cell)
return min_dist
self.mean_home_halite = 100
home_cells = [
cell.halite
for cell in self.halite_cells
if cell_to_yard_dist(cell) <= self.home_grown_cell_dist
]
if home_cells:
self.mean_home_halite = np.mean(home_cells)
# Player info
self.me.total_halite = self.me.halite + cargo(self.me)
self.max_enemy_halite = -1
self.max_enemy = None
for p in self.board.opponents:
p.total_halite = p.halite + cargo(p)
if p.total_halite >= self.max_enemy_halite:
self.max_enemy_halite = p.halite
self.max_enemy = p
def bomb_enemy_shipyard(self):
"""Having enough farmers, let's send ghost to enemy shipyard."""
def estimate_halite(player):
h = player.halite
s = len(player.ship_ids) * self.c.spawn_cost
return h + s
MIN_ENEMY_YARD_TO_MY_YARD = 5
def max_bomb_dist():
# Don't use bomb if ship group is small.
if self.num_ships <= 15:
return 0
# Only attack nearby enemy yard.
return MIN_ENEMY_YARD_TO_MY_YARD
def is_near_my_shipyard(enemy_yard):
for yard in self.shipyards:
dist = self.manhattan_dist(yard, enemy_yard)
if dist <= max_bomb_dist():
return True
return False
def non_targeted_enemy_shipyards():
for y in self.enemy_shipyards:
if y.cell.is_targetd:
continue
if not is_near_my_shipyard(y):
continue
yield y
def select_bomb_ship(enemy_yard):
min_dist = 99999
bomb_ship = None
for ship in self.my_idle_ships:
# Don't send halite to enemy.
if ship.halite > 0:
continue
dist = self.manhattan_dist(enemy_yard, ship)
if dist < min_dist:
min_dist = dist
bomb_ship = ship
return min_dist, bomb_ship, enemy_yard
if self.step < BEGINNING_PHRASE_END_STEP:
return
enemy_shipyards = (
select_bomb_ship(y) for y in non_targeted_enemy_shipyards())
enemy_shipyards = [x for x in enemy_shipyards if x[1]]
enemy_shipyards.sort(key=lambda x: x[0])
for _, bomb_ship, enemy_yard in enemy_shipyards:
self.assign_task(bomb_ship, enemy_yard.cell, ShipTask.ATTACK_SHIPYARD)
# One bomb at a time
break
def convert_shipyard(self):
"""Builds shipyard to maximize the total number of halite covered within
|home_grown_cell_dist|."""
MAX_SHIPYARD_NUM = 16
MANHATTAN_DIST_RANGE = range(6, 7+1)
AXIS_DIST_RANGE1 = range(3, 5+1)
AXIS_DIST_RANGE2 = range(1, 5+1)
MAX_SHIP_TO_SHIPYARD_DIST = 8
HALITE_CELL_PER_SHIP = 2.5 if self.step < 60 else 2.8
self.halite_ratio = -1
# No ship left.
if not self.num_ships:
return
def shipyard_num_by_ship_num():
if self.num_ships >= 12:
return min(2 + max((self.num_ships - 12) // 6, 0), MAX_SHIPYARD_NUM)
return 1
def shipyard_num_by_halite_ratio():
num_halite_cells = 0
for cell in self.halite_cells:
min_dist, _ = self.get_nearest_home_yard(cell)
if min_dist <= self.home_grown_cell_dist:
num_halite_cells += 1
num_yards = self.num_shipyards
halite_ratio = num_halite_cells / (self.num_ships or 1)
self.num_home_halite_cells = num_halite_cells
self.halite_ratio = halite_ratio
if halite_ratio < HALITE_CELL_PER_SHIP and self.num_ships >= 15:
num_yards += 1
print('more ship: halite cell / ship =', halite_ratio)
return num_yards
def max_shipyard_num():
return max(shipyard_num_by_ship_num(), shipyard_num_by_halite_ratio())
# Reach max shipyard num.
if self.num_shipyards >= max_shipyard_num():
return
def convert_threshold():
threshold = MIN_HALITE_TO_BUILD_SHIPYARD
# Use as much as I can.
if (self.num_shipyards == 0 or
self.board.step <= BEGINNING_PHRASE_END_STEP or
self.num_ships <= MAX_SHIP_NUM):
threshold = self.c.convert_cost
return max(self.c.convert_cost, threshold)
def has_enough_halite(ship):
return ship.halite + self.me.halite >= convert_threshold()
# return self.me_halite >= convert_threshold()
def has_enemy_nearby(cell):
return any(
has_enemy_ship(c, self.me)
for c in get_neighbor_cells(cell, include_self=True))
def within_predefined_range(cell):
if not self.me.shipyard_ids:
return True
dist_yards = [(self.manhattan_dist(y, cell), y)
for y in self.shipyards]
dist_yards = sorted(dist_yards, key=lambda x: x[0])
for dist, yard in dist_yards[:2]:
if dist not in MANHATTAN_DIST_RANGE:
return False
dist_x, dist_y = axis_manhattan_dists(cell.position, yard.position,
self.c.size)
axis_dist_range = (AXIS_DIST_RANGE1
if self.num_shipyards == 1 else AXIS_DIST_RANGE2)
# That satisfy some axis distance constraints to make me feel safe.
if dist_x not in axis_dist_range or dist_y not in axis_dist_range:
return False
return True
def compute_convert_score(candidate_cell):
MAX_COVER_HALITE = 2
# Maximize the total value of halite when converting ship.
total_cell = 0
total_halite = 0
shipyards = self.shipyards + [candidate_cell] # Fake the cell as shipyard.
for cell in self.halite_cells:
if cell.position == candidate_cell.position:
continue
min_dist, _ = self.get_nearest_home_yard(cell)
if min_dist > self.home_grown_cell_dist:
continue
covered = 0
for dist, yard in cell.nearest_home_yards[:MAX_COVER_HALITE]:
if dist <= self.home_grown_cell_dist:
# Repeat count halite if recovered.
# total_halite += 1.0 / np.sqrt(dist)
total_halite += cell.halite / dist
# total_halite += 1.0 / dist
covered = 1
total_cell += covered
return total_halite, total_cell
def nominate_shipyard_positions():
for cell in self.board.cells.values():
# Exclude existing shipyard position (including enemy ones).
if cell.shipyard_id:
continue
if not within_predefined_range(cell):
continue
# Have a nearby ship.
dist_to_yard, _ = self.find_nearest_enemy(cell, self.ships)
if dist_to_yard > MAX_SHIP_TO_SHIPYARD_DIST:
continue
cell.convert_score = compute_convert_score(cell)
yield cell
def convert_ship(ship):
self.cost_halite += (self.c.convert_cost - ship.halite)
ship.next_action = ShipAction.CONVERT
ship.has_assignment = True
ship.cell.is_targetd = True
def call_for_ship(cell):
ships = sorted(self.ships, key=lambda s: self.manhattan_dist(s, cell))
for ship in ships:
if not has_enough_halite(ship):
continue
dist_to_yard = self.manhattan_dist(ship, cell)
# Annoy nearby enemy.
min_enemy_to_yard_dist, min_enemy = self.find_nearest_enemy(cell, self.enemy_ships)
if (min_enemy and min_enemy_to_yard_dist <= dist_to_yard
and min_enemy.halite < ship.halite):
continue
if ship.position == cell.position and not has_enemy_nearby(ship.cell):
convert_ship(ship)
return True
if ship.position != cell.position:
print("Send ship(%s %s) to shipyard position (%s), dist=%s" % (ship.id, ship.position, cell.position, dist_to_yard))
# Let's use GOTO_HALITE for now.
self.assign_task(ship, cell, ShipTask.INITIAL_SHIPYARD)
return True
return False
# Pre cash money for shipyard conversion when moving towards.
self.save_for_converting = self.c.convert_cost
candidate_cells = list(nominate_shipyard_positions())
if not candidate_cells:
return
candidate_cells.sort(key=lambda c: c.convert_score, reverse=True)
for cell in candidate_cells:
if call_for_ship(cell):
# One shipyard at a time.
return
def compute_ship_moves(self):
"""Computes ship moves to its target.
Maximize total expected value.
* prefer the move to the target (distance is shorter)
* not prefer move into enemy with lower halite (or equal)
"""
spawn_cost = self.board.configuration.spawn_cost
convert_cost = self.board.configuration.convert_cost
collect_rate = self.board.configuration.collect_rate
def compute_weight(ship, next_position):
ignore_neighbour_cell_enemy = False
target_cell = ship.target_cell
next_cell = self.board[next_position]
# If a non-followed ship's next move is to alley SPAWNING shipyard, skip
yard = next_cell.shipyard
if (yard and yard.player_id == self.me.id and
yard.next_action == ShipyardAction.SPAWN and
not hasattr(ship, "follower")):
return MIN_WEIGHT
# If stay at current location, prefer not stay...
dist = manhattan_dist(next_position, target_cell.position, self.c.size)
ship_dist = self.manhattan_dist(ship, target_cell)
wt = ship_dist - dist
if (ship.task_type == ShipTask.STAY and ship.position == next_position):
wt -= 10
# If collecting halite
if ((ship.task_type == ShipTask.GOTO_HALITE or
ship.task_type == ShipTask.COLLECT) and target_cell.halite > 0):
ship_to_poi = dist
poi_to_yard, min_yard = self.get_nearest_home_yard(next_cell)
if min_yard is None:
poi_to_yard = 1
expect_return = self.halite_per_turn(ship, target_cell, ship_to_poi,
poi_to_yard)
if expect_return < 0:
return MIN_WEIGHT
wt += expect_return
# If go back home
if ship.task_type == ShipTask.RETURN:
wt += ship.halite / (dist + 1)
if hasattr(ship, 'follower'):
wt += self.c.spawn_cost
# If goto enemy yard.
if ship.task_type == ShipTask.ATTACK_SHIPYARD:
# TODO: use what value as weight for destory enemy yard?
wt += convert_cost / (dist + 1)
# Do not step on shipyard
if (ship.task_type != ShipTask.ATTACK_SHIPYARD and
next_cell.shipyard_id and next_cell.shipyard.player_id != self.me.id):
wt -= convert_cost / (dist + 1)
if ship.task_type == ShipTask.ATTACK_SHIP:
wt += convert_cost / (dist + 1)
enemy = ship.target_enemy
enemy_dist = manhattan_dist(next_position, enemy.position, self.c.size)
wt += enemy.halite / (enemy_dist + 1)
if ship.task_type == ShipTask.GUARD_SHIPYARD:
wt += 1 / (dist + 1)
# Only ignore enemy when the ship is on the yard.
if next_position == target_cell.position:
ignore_neighbour_cell_enemy = True
def move_away_from_enemy(enemy, ship, avoid_collision=True):
"""Collides with enemy if my ship has less halite."""
if ship.halite < enemy.halite:
return False
elif ship.halite > enemy.halite:
return True
# enemy.halite == ship.halite
assert enemy.halite == ship.halite
if ship.halite > 0:
return True
if avoid_collision:
return True
return random.random() < AVOID_COLLIDE_RATIO
# If there is an enemy in next_position with lower halite
if has_enemy_ship(next_cell, self.me):
# If there is an enemy sitting on its shipyard, collide with him.
if (ship.task_type == ShipTask.ATTACK_SHIPYARD and
next_cell.position == target_cell.position):
pass
elif move_away_from_enemy(next_cell.ship, ship):
wt -= (spawn_cost + ship.halite)
# If there is an enemy in neighbor next_position with lower halite
if not ignore_neighbour_cell_enemy:
for nb_cell in get_neighbor_cells(next_cell):
if has_enemy_ship(nb_cell, self.me):
if move_away_from_enemy(nb_cell.ship, ship, avoid_collision=False):
wt -= (spawn_cost + ship.halite)
return wt
# Skip only convert ships.
ships = [s for s in self.ships if not s.next_action]
next_positions = {make_move(s.position, move, self.c.size)
for s in ships for move in POSSIBLE_MOVES}
position_to_index = {pos : i for i, pos in enumerate(next_positions)}
C = np.ones((len(ships), len(next_positions))) * MIN_WEIGHT
for ship_idx, ship in enumerate(ships):
for move in POSSIBLE_MOVES:
next_position = make_move(ship.position, move, self.c.size)
poi_idx = position_to_index[next_position]
C[ship_idx, poi_idx] = compute_weight(ship, next_position)
rows, cols = scipy.optimize.linear_sum_assignment(C, maximize=True)
index_to_position = list(next_positions)
for ship_idx, poi_idx in zip(rows, cols):
ship = ships[ship_idx]
next_position = index_to_position[poi_idx]
ship.next_cell = self.board[next_position]
ship.next_action = direction_to_ship_action(ship.position, next_position,
self.c.size)
# print(ship.id, 'at', ship.position, 'goto', next_position)
if len(rows) != len(ships):
matched_ship_ids = set()
for ship_idx in rows:
matched_ship_ids.add(ships[ship_idx].id)
for ship in ships: