-
Notifications
You must be signed in to change notification settings - Fork 4
/
tetris.aca
421 lines (392 loc) · 13.2 KB
/
tetris.aca
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
#*
* Tetris Game in Minecraft!
*
* Features:
* 7 pieces; a simple rotation system; left, right control
*
* Compile with:
* cd path/to/acacia
* python acacia.py test/demo/tetris.aca
* Create a super flat world, load the generated behavior pack
* and type in chat:
* /function main
* (The above one only needs to be ran once, the following command will
* start or restart the game.)
* /function start
* Everything will be set up by Acacia!
* Press A to go left, D to go right, W to rotate.
*#
import schedule
import print
import math
import world
const (
UPDATE_INTERVAL = 8, # ticks
WIDTH = 10, HEIGHT = 20,
ORIGIN_POS = AbsPos(0, -50, 0),
PLAYER_POS = ORIGIN_POS.offset(x=WIDTH/2, y=+20, z=HEIGHT/2),
BLOCK_EMPTY = world.Block("concrete", {"color": "black"})
)
const (
SHAPE_I = 0,
SHAPE_L = 1,
SHAPE_O = 2,
SHAPE_T = 3,
SHAPE_S = 4,
SHAPE_Z = 5,
SHAPE_J = 6
)
const (
ROT_NONE = 0,
ROT_CLOCKWISE = 1,
ROT_180 = 2,
ROT_CCLOCKWISE = 3
)
const SHAPE_COLORS = {
SHAPE_I: "cyan",
SHAPE_L: "orange",
SHAPE_O: "yellow",
SHAPE_T: "purple",
SHAPE_S: "lime",
SHAPE_Z: "red",
SHAPE_J: "blue"
}
const SHAPE_SIZES = {
# width, height (when not rotated)
SHAPE_I: {4, 1},
SHAPE_L: {3, 2},
SHAPE_O: {2, 2},
SHAPE_T: {3, 2},
SHAPE_S: {3, 2},
SHAPE_Z: {3, 2},
SHAPE_J: {3, 2}
}
const SHAPE_ROTATE_MOVE = {
# (Shapes not in this list does not move when rotated)
{SHAPE_I}: { # 1x4
ROT_NONE: {+2, -1}, ROT_CLOCKWISE: {-2, +2},
ROT_180: {+1, -2}, ROT_CCLOCKWISE: {-1, +1}
},
{SHAPE_L, SHAPE_J, SHAPE_T, SHAPE_S, SHAPE_Z}: { # 2x3
ROT_NONE: {+1, 0}, ROT_CLOCKWISE: {-1, +1},
ROT_180: {0, -1}, ROT_CCLOCKWISE: {0, 0}
}
}
const SHAPE_PATTERNS_360 = {
SHAPE_L: {
ROT_NONE: {{0, 1}, {1, 1}, {2, 1}, {2, 0}},
ROT_CLOCKWISE: {{0, 0}, {0, 1}, {0, 2}, {1, 2}},
ROT_180: {{0, 0}, {1, 0}, {2, 0}, {0, 1}},
ROT_CCLOCKWISE: {{0, 0}, {1, 0}, {1, 1}, {1, 2}}
},
SHAPE_T: {
ROT_NONE: {{0, 1}, {1, 1}, {2, 1}, {1, 0}},
ROT_CLOCKWISE: {{0, 0}, {0, 1}, {0, 2}, {1, 1}},
ROT_180: {{0, 0}, {1, 0}, {2, 0}, {1, 1}},
ROT_CCLOCKWISE: {{1, 0}, {1, 1}, {1, 2}, {0, 1}},
},
SHAPE_J: {
ROT_NONE: {{0, 0}, {0, 1}, {1, 1}, {2, 1}},
ROT_CLOCKWISE: {{0, 0}, {1, 0}, {0, 1}, {0, 2}},
ROT_180: {{0, 0}, {1, 0}, {2, 0}, {2, 1}},
ROT_CCLOCKWISE: {{1, 0}, {1, 1}, {1, 2}, {0, 2}}
}
}
const SHAPE_PATTERNS_180 = {
SHAPE_S: {
{{0, 1}, {1, 1}, {1, 0}, {2, 0}}, # no rotation
{{0, 0}, {0, 1}, {1, 1}, {1, 2}}, # 90 degrees
},
SHAPE_Z: {
{{0, 0}, {1, 0}, {1, 1}, {2, 1}},
{{1, 0}, {1, 1}, {0, 1}, {0, 2}}
},
SHAPE_I: {
{{0, 0}, {1, 0}, {2, 0}, {3, 0}},
{{0, 0}, {0, 1}, {0, 2}, {0, 3}}
}
}
const SHAPE_PATTERNS_90 = {
SHAPE_O: {{0, 0}, {0, 1}, {1, 1}, {1, 0}}
}
struct Size:
height: int
width: int
entity PosDummy:
#* Dummy entity that just represnts a position. *#
new():
new(type="armor_stand", pos=ORIGIN_POS)
world.effect_give(
self, "invisibility", duration=int.MAX, particle=False
)
inline def move(const row: int = 0, const col: int = 0):
#* Move the dummy. *#
world.move(self, z=row, x=col)
def go_to(row: int, col: int):
#* Teleport dummy to given place. *#
world.tp(self, ORIGIN_POS)
i: int = row
while i > 0:
self.move(row=+1)
i -= 1
i = col
while i > 0:
self.move(col=+1)
i -= 1
shape_row: int
shape_col: int
shape_type: int
shape_rotation: int
running: bool = False
score: int = 0
player_group := Engroup[Entity]()
dummy_group := Engroup[PosDummy]()
def get_dummy() -> PosDummy:
#*
Get the dummy entity which only exists when game is running.
We should have `dummy_group.size() == 1 and running`.
*#
result dummy_group.to_single()
def get_shape_size(shape: int, rotation: int) -> Size:
#* Get real shape size, with rotation considered. *#
for s in SHAPE_SIZES:
if shape == s:
const size = SHAPE_SIZES[s]
if rotation == ROT_CLOCKWISE or rotation == ROT_CCLOCKWISE:
# When rotated, width is height, vice versa
result Size(height=size[0], width=size[1])
else:
result Size(width=size[0], height=size[1])
inline def traverse_shape(
dummy: PosDummy, shape: int, rotation: int, const action
):
#*
Find all blocks in a shape (relative to `dummy`'s position) and
execute function `action` with each position of block as argument.
*#
inline def do_action(const list2d):
for xz in list2d:
action(Pos(dummy).offset(x=xz[0], z=xz[1]))
for s in SHAPE_PATTERNS_360:
if shape == s:
const shape_def = SHAPE_PATTERNS_360[s]
for r in shape_def:
if rotation == r:
do_action(shape_def[r])
for s in SHAPE_PATTERNS_180:
if shape == s:
const shape_def = SHAPE_PATTERNS_180[s]
if rotation == ROT_NONE or rotation == ROT_180:
do_action(shape_def[0])
else:
do_action(shape_def[1])
for s in SHAPE_PATTERNS_90:
if shape == s:
do_action(SHAPE_PATTERNS_90[s])
def draw_shape(dummy: PosDummy, shape: int, rotation: int):
#* Render current shape at `dummy`. *#
inline def place_block(const pos: Pos):
for s in SHAPE_COLORS:
if shape == s:
const block = world.Block(
"concrete", {"color": SHAPE_COLORS[s]}
)
world.setblock(pos, block)
traverse_shape(dummy, shape, rotation, action=place_block)
def clear_shape(dummy: PosDummy, shape: int, rotation: int):
#* Clear the shape (i.e. place empty block). *#
inline def empty_block(const pos: Pos):
world.setblock(pos, BLOCK_EMPTY)
traverse_shape(dummy, shape, rotation, action=empty_block)
def will_collide(dummy: PosDummy, shape: int, rotation: int) -> bool:
#*
Whether the shape with given rotation will collide with other
blocks at dummy.
*#
res := False
inline def check(const pos: Pos):
res = res or not world.is_block(pos, BLOCK_EMPTY)
traverse_shape(dummy, shape, rotation, action=check)
result res
def game_over():
running = False
world.kill(dummy_group)
print.title("Game Over!", player_group)
print.title(print.format("Lines: %0", score), player_group,
mode=print.SUBTITLE)
player_group.clear()
def new_shape():
#* Create a new active shape. *#
shape_type = math.randint(0, 6)
shape_row = 0
shape_col = WIDTH / 2
shape_rotation = ROT_NONE
dummy := get_dummy()
dummy.go_to(shape_row, shape_col)
if will_collide(dummy, shape_type, shape_rotation):
game_over()
def check_completion(shape_height: int):
#*
Check line completion. This must be called right after the shape
landed and this relies on the landed shape information.
Note: this function moves the global dummy.
*#
dummy := get_dummy()
y: int = 0
while y < shape_height:
dummy.go_to(shape_row + y, 0)
x: int = 0
got_empty: bool = False
while x < WIDTH:
if world.is_block(Pos(dummy), BLOCK_EMPTY):
got_empty = True
x += 1
dummy.move(col=+1)
if not got_empty:
# Clear this line!
score += 1
# Dummy is out of bound on the right by 1 block now,
# so move it back by 1 block
dummy.move(col=-1)
# 1. move lines above cleared line down by 1 block
dummy.move(row=-1)
world.clone(origin=Pos(dummy), offset=ORIGIN_POS,
dest=ORIGIN_POS.offset(z=+1), mode="force")
# 2. fill the top line with empty
world.fill(
origin=ORIGIN_POS, offset=Offset(x=WIDTH-1),
block=BLOCK_EMPTY
)
y += 1
def tick():
if running:
dummy := get_dummy()
# 1. Decide whether shape has landed
# Land on field bottom
shape_height: int = get_shape_size(shape_type, shape_rotation).height
landed: bool = shape_height + shape_row >= HEIGHT
# Land on another tetromino
# We need to clear origin piece first or the new piece will
# always collide with the old piece.
dummy.go_to(shape_row, shape_col)
clear_shape(dummy, shape_type, shape_rotation)
# Assume that shape moves 1 block more...
dummy.move(row=+1)
landed = landed or will_collide(dummy, shape_type, shape_rotation)
dummy.move(row=-1)
# 2. If landed, check completion and create new piece
if landed:
# We clear origin piece above to check collision, now that
# the piece has landed, we will need to put it back.
draw_shape(dummy, shape_type, shape_rotation)
check_completion(shape_height)
new_shape()
# 3. If not, destroy the original blocks and create new blocks
# one block lower.
else:
dummy.move(row=+1)
shape_row += 1
# 4. Redraw shape
draw_shape(dummy, shape_type, shape_rotation)
def start():
#* Start the game loop! *#
running = True
score = 0
new_shape()
def rotate():
#*
Try to rotate the shape.
A rotation consists of the change of shape AND the move of shape.
See https://tetris.fandom.com/wiki/SRS for details.
*#
# Rotation IDs are 1, 2, 3, 4; when 5 is reached, modulo by 4 to
# get back to 1.
new_rotation: int = (shape_rotation + 1) % 4
# Move the shape
new_col: int = shape_col
new_row: int = shape_row
for types in SHAPE_ROTATE_MOVE:
is_contained: bool = False
for type in types:
if type == shape_type:
is_contained = True
if is_contained:
const rot_def = SHAPE_ROTATE_MOVE[types]
for rotation in rot_def:
if rotation == shape_rotation:
const rowcol = rot_def[rotation]
new_col += rowcol[0]
new_row += rowcol[1]
# Make sure the shape is not out of game field after rotation
# This is a simple alternative to Wall Kicks (see link above).
new_size := get_shape_size(shape_type, new_rotation)
new_col = math.min(new_col, WIDTH - new_size.width)
new_col = math.max(new_col, 0)
new_row = math.min(new_row, HEIGHT - new_size.height)
new_row = math.max(new_row, 0)
dummy := get_dummy()
dummy.go_to(shape_row, shape_col)
clear_shape(dummy, shape_type, shape_rotation)
dummy.go_to(new_row, new_col)
# Only update when no collision will happen
if not will_collide(dummy, shape_type, new_rotation):
shape_rotation = new_rotation
shape_col = new_col
shape_row = new_row
dummy.go_to(shape_row, shape_col)
draw_shape(dummy, shape_type, shape_rotation)
def _update_col(col_offset: int):
#* Implementation of `left` and `right`. *#
new_col: int = shape_col + col_offset
# Only continue when in the bound
if 0 <= new_col < WIDTH:
# Only continue when no collision will happen
dummy := get_dummy()
dummy.go_to(shape_row, shape_col)
clear_shape(dummy, shape_type, shape_rotation)
dummy.go_to(shape_row, new_col)
if not will_collide(dummy, shape_type, shape_rotation):
shape_col = new_col
dummy.go_to(shape_row, shape_col)
draw_shape(dummy, shape_type, shape_rotation)
def left():
#* Try to move left. *#
_update_col(col_offset=-1)
def right():
#* Try to move right. *#
_update_col(col_offset=+1)
def check_input():
#* Check player input. *#
if running:
const (
pos_left = PLAYER_POS.offset(x=-1.05),
pos_right = PLAYER_POS.offset(x=1.05),
pos_front = PLAYER_POS.offset(z=-1.05)
)
player := player_group.to_single()
if world.is_entity(player, Enfilter().distance_from(pos_left, max=1)):
left()
if world.is_entity(player, Enfilter().distance_from(pos_right, max=1)):
right()
if world.is_entity(player, Enfilter().distance_from(pos_front, max=1)):
rotate()
if not player_group.is_empty():
world.tp(player_group, PLAYER_POS)
world.rotate(player_group, Rot(-180, 90))
schedule.register_loop(tick, interval=UPDATE_INTERVAL)
schedule.register_loop(check_input, interval=2)
interface start:
#* Program entry: (re)start Tetris! *#
dummy := PosDummy()
dummy_group.add(dummy)
player_group.select(Enfilter().is_type("player").nearest_from(PLAYER_POS))
print.title("Tetris Game!!!", target=player_group,
fade_in=0, fade_out=0, stay_time=50)
world.fill(ORIGIN_POS, Offset(x=WIDTH-1, z=HEIGHT-1), BLOCK_EMPTY)
# Place a block so that player won't fall
world.setblock(PLAYER_POS.offset(y=-1), world.Block("barrier"))
# Delay 60 ticks before starting
schedule.Task(start).after(60)
interface game_over:
#* Stop the game at once. *#
game_over()