-
Notifications
You must be signed in to change notification settings - Fork 0
/
tetris_ai.py
300 lines (229 loc) · 8.64 KB
/
tetris_ai.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
#!/usr/bin/python
# coding=utf-8
"""
Tetris AI: A Self-playing Tetris
Copyright (C) 2016 Daogan Ao <https://github.com/daogan>
Date: 2016-12-27
"""
import time
from tetris import Tetris
from ai_config import *
from tetris_config import START_X, START_Y, LEFT, RIGHT, DOWN, ROTATE,\
DROP, NEXT, WIDTH, HEIGHT, EMPTY
class TetrisAI(object):
def __init__(self):
self.tetro_queue = [0, 0]
# for tracking the optimal rotation and dropping position
self.max_score = 0
self.n_rotations = 0
self.dropping_x = START_X
# for tracking the states of the search queue
self.tetro_rots = [0] * LOOK_AHEAD
self.tetros = [0] * LOOK_AHEAD
self.tetro_xs = [0] * LOOK_AHEAD
def play(self, tetris):
tetris.set_timeout(TIMEOUT)
while tetris.running:
tetris.handle_keyboard()
if tetris.pause:
continue
self.init_states(tetris)
self.search(tetris, level=0, score=0)
actions = self.get_actions()
for action in actions:
if action == DROP:
tetris.drop()
else:
tetris.move(action)
tetris.render()
time.sleep(tetris.timeout/1000.0)
n_lines = 0
if not tetris.move(DOWN):
if tetris.pos_y == START_Y:
tetris.die()
else:
tetris.collide()
n_lines = tetris.clear_lines()
tetris.scoring(n_lines)
tetris.spawn_next()
if n_lines > 0:
tetris.render()
tetris.finish()
def search(self, tetris, level, score):
"""
Depth first search, recursively search for the optimal action sequence
with the highest score.
"""
# we have reached the stopping condition of the recursive search
if level == LOOK_AHEAD:
if score > self.max_score:
self.max_score = score
self.n_rotations = self.tetro_rots[0]
self.dropping_x = self.tetro_xs[0]
return
# reset to START_Y in deeper search levels
if level > 0:
tetris.pos_y = START_Y
# remember the initial tetris states
t_tetrimino = tetris.tetromino
t_pos_y = tetris.pos_y
t_pos_x = tetris.pos_x
current = self.tetro_queue[level]
for rotation, tetro in enumerate(self.each(current)):
self.tetro_rots[level] = rotation
self.tetros[level] = tetro
for pos_x in self.range_x(tetro):
self.tetro_xs[level] = pos_x
# since the board (and some other internal variables) is
# shared and modified during the recursive search, we need
# to remember the current board states for later restoration
t_board = [row[:] for row in tetris.board]
t_n_fills = tetris.n_fills[:]
tetris.tetromino = tetro
tetris.pos_x = pos_x
tetris.drop()
tetris.collide()
nscore = self.heuristic(tetris)
self.search(tetris, level+1, score+nscore)
# restore previous context
tetris.board = t_board
tetris.n_fills = t_n_fills
tetris.pos_y = t_pos_y
# restore the initial tetris states after the search
tetris.tetromino = t_tetrimino
tetris.pos_x = t_pos_x
def each(self, tetro):
"""
Return all rotated pieces of a given tetromino.
"""
yield tetro
current = tetro
tetro = NEXT[tetro]
# stop when rotated back to the original tetromino
while tetro != current:
yield tetro
# make a rotation
tetro = NEXT[tetro]
def range_x(self, tetro):
"""
Return the range of the left and right-most horizontal position
current tetromino can travel on the board.
"""
# range: [s, e)
return range(-LX[tetro], WIDTH-4+RX[tetro]+1)
def heuristic(self, tetris):
"""
Evaluate the score of current board.
This scoring heuristic is base on the Pierre Dellacheries algorithm,
read more details at:
ielashi.com/el-tetris-an-improvement-on-pierre-dellacheries-algorithm
TODO: Experiment with different or more sophisticated heuristics.
"""
landing_y = tetris.pos_y
row_trans, col_trans = self.get_transitions(tetris.board)
n_holes = self.get_holes(tetris.board)
wells_height = self.get_wells_height(tetris.board)
n_lines = tetris.clear_lines()
# n_lines = len([i for i in tetris.n_fills if i == WIDTH])
t = tetris.tetromino
landing_height = HEIGHT - landing_y - TH[t][0] + 0.5*TH[t][1]
score = A*landing_height + B*n_lines + C*row_trans
score += D*col_trans + E*n_holes + F*wells_height
return score
def get_actions(self):
actions = [ROTATE] * self.n_rotations
steps = self.dropping_x - START_X
if steps > 0:
actions += [RIGHT] * steps
elif steps < 0:
actions += [LEFT] * (0 - steps)
# append final drop
return actions + [DROP]
def init_states(self, tetris):
self.max_score = -(1 << 30)
self.tetro_queue[0] = tetris.tetromino
self.tetro_queue[1] = tetris.next_tetromino
def get_holes(self, board):
"""
Get the total number of holes in the board.
"""
holes = 0
for x in range(WIDTH):
occuppied = False
for y in range(HEIGHT):
if board[y][x] != EMPTY:
if not occuppied:
occuppied = True
elif occuppied:
holes += 1
return holes
def get_transitions(self, board):
"""
Get the total number of row and column transitions.
A row transition occurs when a filled cell is adjacent to an empty
cell on the same row and vice versa. The outer regions of the board
are considered to be filled.
example:
0011101001 ==> row transitions is 6
^ ^ ^^^ ^
1111100000 ==> row transitions is 2
^ ^
"""
row_trans = 0
for y in range(HEIGHT):
filled = True
for x in range(WIDTH):
if filled != (board[y][x] != EMPTY):
row_trans += 1
filled = not filled
if not filled:
row_trans += 1
col_trans = 0
for x in range(WIDTH):
filled = True
for y in range(HEIGHT):
if filled != (board[y][x] != EMPTY):
col_trans += 1
filled = not filled
if not filled:
col_trans += 1
return row_trans, col_trans
def get_wells_height(self, board):
"""
Get the total wells height.
A well is a sequence of consecutive empty cells on the same column
where the top empty cell is surrounded by filled cells or border of
the board.
For a well of depth n, the height is defined as 1 + 2 + ... + n,
this gives more panalty to deeper wells.
"""
wells_height = 0
for x in range(1, WIDTH-1):
for y in range(HEIGHT):
if board[y][x] == EMPTY and board[y][x-1] != EMPTY \
and board[y][x+1] != EMPTY:
wells_height += 1
for _y in range(y+1, HEIGHT):
if board[_y][x] != EMPTY:
break
wells_height += 1
for y in range(HEIGHT):
# check wells in the leftmost boarder of the board
if board[y][0] == EMPTY and board[y][1] != EMPTY:
wells_height += 1
for _y in range(y+1, HEIGHT):
if board[_y][x] != EMPTY:
break
wells_height += 1
# check wells in the rightmost border of the board
if board[y][WIDTH-1] == EMPTY and board[y][WIDTH-2] != EMPTY:
wells_height += 1
for _y in range(y+1, HEIGHT):
if board[_y][x] != EMPTY:
break
wells_height += 1
return wells_height
if __name__ == '__main__':
tetris = Tetris(autoboot=False)
ai = TetrisAI()
ai.play(tetris)