ICFPC 2015: Hexagonal Tetris
This year the problem was about writing an AI to play hexagonal tetris.
Here is brief explanation of our solution.
At high level, we use 2-step approach:
- For current figure using BFS we find the best next position (we call it macro move) according to some heuristic function.
- For that position we generate sequence of moves that maximizes power words score.
Now let’s consider each step in more details
Macro Moves
First we generated a set of all terminal states (macro moves) where the current figure would be locked.
This is done using BFS. Then each state is scored using weighted sum of the following features (see deep_solver.py
for more details):
- Real score. Score that is obtained for locking the figure according to game rules. (E.g. the more lines will be cleared by macro move the higher will be score)
- Gravity score. Sum of
-exp(-y / 20.)
for each (x, y), where (x, y) is filled cell of the board. It encourages putting the figure on the lowest lines. - Covered Holes score. We have 3 features here: number of empty cells that have a filled cell as a top-right neighbor (c1) or top-left neighbor (c2) or both (c3).
- Unfinished score. Sum of
-exp(-num_empty / 6.0)
for each row, wherenum_empty
is number of empty cells in this row. It encourages try to fill lines with the least number of empty cells. - Smoothness score. Sum of
abs(is_empty(x, y) - is_empty(x + 1, y))
for each (x, y) – basically the number of times filled cell is adjacent to empty within a row. It encourages to put figures more compactly (do not leave holes in a row).
To make computation of this scores more efficient they were represented as vector operations over numpy arrays. For example, gravity score could be computed using formula:
grid = np.mgrid[:np_board.shape[0], :np_board.shape[1]].astype(np.int)
ygrid = grid[0]
gravity_score = -np.sum(np_board * np.exp(-ygrid * 0.05))
It’s hard to find balance of this scores, so each map has its own hand-tuned set of coefficients:
# Values for
# Config.DEEPSOLVER_DEPTH, \
# Config.DEEPSOLVER_COEF_GRAVITY, \
# Config.DEEPSOLVER_COEF_HOLE1, \
# Config.DEEPSOLVER_COEF_HOLE2, \
# Config.DEEPSOLVER_COEF_HOLE3, \
# Config.DEEPSOLVER_COEF_UNF_ROW, \
# Config.DEEPSOLVER_COEF_SMOOTH
# respectively
param = {
0: (1, 150, 1, 1, 10, 0.5, 20),
1: (1, 50, 1, 1, 1, 0.5, 2),
2: (1, 20, 4, 4, 1, 0, 2),
3: (1, 20, 4, 4, 1, 0, 2),
4: (1, 70, 1, 1, 3, 0, 2),
5: (1, 70, 1, 1, 3, 0, 2),
6: (1, 0, -0.1, +0.2, 10, -145, 2),
7: (1, 70, 1, 1, 3, 0, 2),
8: (2, 10, 1, -4, 50, 0, 8),
9: (1, 0, -1, -1, 10, 0, 2),
10: (1, 35, 3, -1, 50, -500, 0.4),
11: (2, 50, 1, 1, 8, 0, 5),
12: (1, 50, 1, 1, 8, 0, 5),
13: (1, 50, 1, 1, 8, 0, 5),
14: (1, 50, 1, 1, 8, 0, 5),
15: (2, 50, 1, 1, 1, 0, 1),
16: (1, 50, 1, 1, 1, 0, 1),
17: (1, 10, 0, 0, 0, 0, 0),
18: (1, 50, 1, 1, 1, 0, 1),
19: (1, 50, 1, 1, 1, 0, 1),
20: (1, 50, 0, 0, 1, -3, 1),
21: (1, 50, 0, 0, 1, -3, 1),
22: (1, 70, 1, -10, 10, -460, 4),
23: (3, 15, 0, 0, 0.3, 2, 15),
24: (1, 50, 0, 0, 2, 0, 10),
-1: (1, 50, 1, 1, 1, -100.5, 2),
}
We also implemented a couple of heuristics to speed up BFS (this was especially useful on big maps):
- If the figure is higher than highest filled cell of the map, then use move by formula
Move.SE if current_figure.y % 2 == 0 else Move.SW
- If length of BFS queue is more than 40, then don’t consider rotations when choosing next move
Power words
In previous paragraph we described algorithm to compute the macro move for given figure. Now we will explain how to choose sequence that will put the figure into the position defined by this macro move.
We used BFS that was first considering ComplexMove
-s (sequence of basic moves that form a power word) and then basic moves itself.
As a score function for BFS we were using number of points we get for this sequence.
Words that were not used before have a score 300 + 2 * length
so algorithm is encouraged to try them first.
Implementation details
The coordinate system provided in specification was very inconvenient to use, so we converted everything to our own representation of hexagonal grid.
TODO: explanation picture
We extended the y
-th row by (y + 1) / 2
cells to the left, and by width - (y + 1) / 2
cells to the right so that each row has exactly 2 * width
cells.
We represented each figure by coordinates of pivot + list of (dx, dy) – relative coordinates of points to pivot. Our coordinate system is a linear one, so it’s relatively easy to compute the rotation of any given point (x, y) around pivot:
// Rotation is a affine transformation, to following axioms hold:
R (A + B) = R(A) + R(B)
R (c * A) = c * R(A)
// Now apply it to formula
(y, x) = y * (1, 0) + x * (0, 1)
R( (y, x) ) = R( y * (1, 0) ) + R( x * (0, 1) )
R( (y, x) ) = y * R( (1, 0) ) + x * R( (0, 1) )
// From the system definitions it's clear that
R: (1, 0) -> (0, -1)
R: (0, 1) -> (1, 1)
// And finally
R( (y, x) ) = y * (0, -1) + x * (1, 1) = (x, x - y)
Thus we get the following forumla:
new_y = x
new_x = x - y
Infrastructure
We implemented a bunch of scripts that were useful during contest.
interactive_tetris.py
- script to play any map using keyboard. Was useful in debugging various corner cases of game rules implementation
tester_one.py
- script that was solving given problem and sending the result if its better than latest one. It was very useful to submit solutions with confidence that we will not overwrite the better one.
board_stats.py
- very simple script to output basic statistics about all problems.
Was useful in quick understanding complexity of maps. Example output:
0 || seeds = 1 || figures = 100 || w = 10, h = 10, total = 100
1 || seeds = 1 || figures = 100 || w = 15, h = 15, total = 225
2 || seeds = 10 || figures = 100 || w = 15, h = 30, total = 450
3 || seeds = 5 || figures = 100 || w = 30, h = 20, total = 600
4 || seeds = 50 || figures = 200 || w = 10, h = 15, total = 150
5 || seeds = 10 || figures = 100 || w = 30, h = 20, total = 600
6 || seeds = 50 || figures = 150 || w = 10, h = 10, total = 100
7 || seeds = 5 || figures = 100 || w = 40, h = 20, total = 800
8 || seeds = 10 || figures = 400 || w = 10, h = 8, total = 80
9 || seeds = 5 || figures = 400 || w = 10, h = 8, total = 80
10 || seeds = 1 || figures = 100 || w = 10, h = 7, total = 70
11 || seeds = 5 || figures = 50 || w = 10, h = 10, total = 100
12 || seeds = 10 || figures = 100 || w = 15, h = 20, total = 300
13 || seeds = 1 || figures = 100 || w = 15, h = 20, total = 300
14 || seeds = 1 || figures = 500 || w = 50, h = 50, total = 2500
15 || seeds = 1 || figures = 100 || w = 15, h = 20, total = 300
16 || seeds = 1 || figures = 100 || w = 15, h = 20, total = 300
17 || seeds = 1 || figures = 100 || w = 15, h = 15, total = 225
18 || seeds = 1 || figures = 1000 || w = 30, h = 30, total = 900
19 || seeds = 1 || figures = 100 || w = 15, h = 15, total = 225
20 || seeds = 1 || figures = 100 || w = 15, h = 15, total = 225
21 || seeds = 1 || figures = 20 || w = 10, h = 8, total = 80
22 || seeds = 1 || figures = 100 || w = 10, h = 8, total = 80
23 || seeds = 1 || figures = 100 || w = 10, h = 9, total = 90
24 || seeds = 1 || figures = 1620 || w = 100, h = 40, total = 4000
leader_board.py
- script to get a current state of leader board. It was useful to periodically obtain and monitor the following information:
- Rank and score we currently have
- How much more score we need to have in order to advance
- Tag of our latest submission
Example output:
Ranks files wasn't specified using latest: rankings/0092-o6ql.js
--------------------------------------------------------------------------------
0 || rank=68, score=4999 (next = 12 more) (need n/a) || power = 4 || tag = v3 /0262-ma6b score 4999 5
1 || rank=70, score=2778 (next = 6 more) (need n/a) || power = 2 || tag = v3 /0263-dwc5 score 2778 5
2 || rank=47, score=7176 (next = 40 more) (need n/a) || power = 5 || tag = v3 /0264-8ojg score 7176 5
3 || rank=65, score=3505 (next = 148 more) (need n/a) || power = 5 || tag = v3 /0265-cwy8 score 3505 5
4 || rank=49, score=4996 (next = 71 more) (need n/a) || power = 5 || tag = v3 /0270-6i0g score 4996 5
5 || rank=58, score=2945 (next = 32 more) (need n/a) || power = 4 || tag = v3 /0273-t67x score 3272 5
6 || rank=54, score=7891 (next = 145 more) (need n/a) || power = 5 || tag = v3 /0276-gq1n score 7891 5
7 || rank=65, score=2107 (next = 398 more) (need n/a) || power = 3 || tag = v3 /0274-rafx score 2107 5
8 || rank=81, score=7784 (next = 180 more) (need 12103 more) || power = 2 || tag = v3 /0245-732g score 7784 5
9 || rank=36, score=3751 (next = 59 more) (need n/a) || power = 0 || tag = /0036-4iu7 deep solver score 3751
10 || rank=71, score=2906 (next = 12 more) (need n/a) || power = 1 || tag = v3 /0255-8tay score 2906 5
11 || rank=83, score=1486 (next = 3 more) (need 4519 more) || power = 2 || tag = v3 /0257-u9kl score 1486 5
12 || rank=107, score=1276 (next = 26 more) (need 7699 more) || power = 0 || tag = v3 /0275-9cug score 1276 5
13 || rank=48, score=1795 (next = 2 more) (need n/a) || power = 5 || tag = v2 /0189-qbyf score 1795 5
14 || rank=70, score=9350 (next = 1302 more) (need n/a) || power = 4 || tag = v3 /0277-tgsy score 9350 5
15 || rank=50, score=3562 (next = 0 more) (need n/a) || power = 5 || tag = v3 /0282-zd6y score 3562 5
16 || rank=59, score=6434 (next = 42 more) (need n/a) || power = 5 || tag = v3 /0285-gl91 score 6434 5
17 || rank=37, score=5472 (next = 44 more) (need n/a) || power = 2 || tag = v3 /0286-a018 score 5472 5
18 || rank=92, score=1625 (next = 40 more) (need 13764 more) || power = 1 || tag = v3 /0284-lijc score 1625 5
19 || rank=40, score=5296 (next = 6 more) (need n/a) || power = 2 || tag = v3 /0283-cx2u score 5296 5
20 || rank=95, score=1944 (next = 6 more) (need 5570 more) || power = 0 || tag = v3 /0278-2d0h score 1944 5
21 || rank=39, score=1386 (next = 2 more) (need n/a) || power = 2 || tag = v3 /0279-ta96 score 1386 5
22 || rank=68, score=4394 (next = 126 more) (need n/a) || power = 2 || tag = v3 /0280-el3f score 4394 5
23 || rank=37, score=1338 (next = 3 more) (need n/a) || power = 1 || tag = v3 /0281-axv5 score 1338 5
24 || rank=72, score=5850 (next = 2324 more) (need n/a) || power = 2 || tag = v2 /0214-46fd score 5850 5
--------------------------------------------------------------------------------
Total rank
rank=59, score=1561 || 1481
In 1 ranks score = 1481, delta = 80
[Update] Top 60 of Qualifying Leaderboard
We finished at 59th place.
Reference: http://2015.icfpcontest.org/leaderboards/qualifying/leader_board.html
Rank | Score | Team Name | Power | |
---|---|---|---|---|
1 | 35 | Unagi | 15 | |
2 | 170 | Hack the pool | 13 | |
3 | 182 | WILD BASHKORT MAGES | 10 | |
4 | 214 | ByCode | 12 | |
5 | 246 | THIRTEEN | 16 | |
6 | 273 | Natsubate! | 8 | |
7 | 278 | Gachi | 6 | |
8 | 328 | code-o-matic | 6 | |
9 | 337 | Olympia | 10 | |
10 | 346 | fixstars | 13 | |
11 | 371 | 7th_building | 7 | |
12 | 376 | tbd | 15 | |
13 | 414 | Hack the loop | 12 | |
14 | 487 | Begot | 8 | |
15 | 494 | Trup16 | 10 | |
16 | 495 | El Latino | 7 | |
17 | 517 | Frictionless Bananas | 7 | |
18 | 532 | Ural Nerds | 7 | |
19 | 571 | LL Cthulh J (puppetlabs) | 18 | |
20 | 588 | Play the VR game Bazaar! Coming soon to the Oculus GearVR store. www.templegatesgames. | com | @Temple_Gates #BazaarGame 7 |
21 | 612 | <$><*> | 7 | |
22 | 616 | TaupeGoons | 15 | |
23 | 621 | The Scrolling Stones | 15 | |
24 | 647 | Chudovo | 9 | |
25 | 715 | NP Compete | 8 | |
26 | 724 | kstm.org | 4 | |
27 | 742 | jabber.ru | 5 | |
28 | 745 | Majesty Hash Conquer | 7 | |
29 | 770 | BusyBeaver | 6 | |
30 | 819 | The Blind Hen | 8 | |
31 | 855 | Yahoo knows him | 4 | |
32 | 896 | N6B | 4 | |
33 | 914 | The Cat is #1!! | 13 | |
34 | 915 | GaupeToons | 13 | |
35 | 942 | DiamondPrincess | 1 | |
36 | 966 | swimteam | 7 | |
37 | 997 | Replete With Abstract Joy | 6 | |
38 | 1038 | GD and Alumni | 6 | |
39 | 1079 | KMC | 6 | |
40 | 1129 | Guru Meditation | 9 | |
41 | 1145 | NCPLUG | 7 | |
42 | 1178 | coding_monkeys | 9 | |
43 | 1179 | A Storm of Minds | 8 | |
44 | 1197 | mi-goto | 6 | |
45 | 1212 | Piggybank Sftw. | 5 | |
46 | 1213 | Cult of the Bound Variable | 15 | |
47 | 1243 | lambda-llama | 9 | |
48 | 1246 | Vezoul | 6 | |
49 | 1339 | The sound of lambda | 11 | |
50 | 1384 | Bears ate our brains! | 5 | |
51 | 1392 | quantum | 4 | |
52 | 1394 | Ξίφων and the Physicists | 7 | |
53 | 1429 | Error 404 | 4 | |
54 | 1442 | Rabbit House | 9 | |
55 | 1444 | error404-icfpc | 4 | |
56 | 1446 | I C Frantic People | 5 | |
57 | 1478 | Skobochka | 8 | |
58 | 1481 | solo r6 | 6 | |
59 | 1561 | Snakes vs Lambdas | 5 | |
60 | 1579 | EPFL Mukel Solo | 1 |