-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathblock.py
More file actions
443 lines (371 loc) · 15.2 KB
/
Copy pathblock.py
File metadata and controls
443 lines (371 loc) · 15.2 KB
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
from __future__ import annotations
import random
import math
from settings import colour_name, COLOUR_LIST
# constants
ROT_CW = 1
ROT_CCW = 3
SWAP_HORZ = 0
SWAP_VERT = 1
def _block_to_squares(board: Block) -> \
list[tuple[tuple[int, int, int], tuple[int, int], int]]:
"""Return a list of tuples describing all the squares that must be drawn
in order to render this Block.
For every undivided Block, the list must contain one tuple that describes
the square to draw for that Block. Each tuple contains:
- the colour of the block,
- the (x, y) coordinates of the top left corner of the block, and
- the size of the block,
in that order.
The order of the tuples does not matter.
"""
if not board.children:
return [(board.colour, board.position, board.size)]
else:
blocks = []
for child in board.children:
blocks += _block_to_squares(child)
return blocks
def generate_board(max_depth: int, size: int) -> Block:
"""Return a new game board with a depth of <max_depth> and dimensions of
<size> by <size>.
>>> board = generate_board(3, 750)
>>> board.max_depth
3
>>> board.size
750
>>> len(board.children) == 4
True
"""
board = Block((0, 0), size, random.choice(COLOUR_LIST), 0, max_depth)
board.smash()
return board
class Block:
"""A square Block in the Blocky game, represented as a tree.
In addition to its tree-related attributes, a Block also contains attributes
that describe how the Block appears on a Cartesian plane. All positions
describe the upper left corner (x, y), and the origin is at (0, 0). All
positions and sizes are in the unit of pixels.
When a block has four children, the order of its children impacts each
child's position. Indices 0, 1, 2, and 3 are the upper-right child,
upper-left child, lower-left child, and lower-right child, respectively.
Attributes
- position: The (x, y) coordinates of the upper left corner of this Block.
- size: The height and width of this square Block.
- colour: If this block is not subdivided, <colour> stores its colour.
Otherwise, <colour> is None.
- level: The level of this block within the overall block structure.
The outermost block, corresponding to the root of the tree,
is at level zero. If a block is at level i, its children are at
level i+1.
- max_depth: The deepest level allowed in the overall block structure.
- children: The blocks into which this block is subdivided. The children are
stored in this order: upper-right child, upper-left child,
lower-left child, lower-right child.
Representation Invariants:
- self.level <= self.max_depth
- len(self.children) == 0 or len(self.children) == 4
- If this Block has children:
- their max_depth is the same as that of this Block.
- their size is half that of this Block.
- their level is one greater than that of this Block.
- their position is determined by the position and size of this Block,
and their index in this Block's list of children.
- this Block's colour is None.
- If this Block has no children:
- its colour is not None.
"""
position: tuple[int, int]
size: int
colour: tuple[int, int, int] | None
level: int
max_depth: int
children: list[Block]
def __init__(self, position: tuple[int, int], size: int,
colour: tuple[int, int, int] | None, level: int,
max_depth: int) -> None:
"""Initialize this block with <position>, dimensions <size> by <size>,
the given <colour>, at <level>, and with no children.
Preconditions:
- position[0] >= 0 and position[1] >= 0
- size > 0
- level >= 0
- max_depth >= level
>>> block = Block((0, 0), 750, (0, 0, 0), 0, 1)
>>> block.position
(0, 0)
>>> block.size
750
>>> block.colour
(0, 0, 0)
>>> block.level
0
>>> block.max_depth
1
"""
self.position = position
self.size = size
self.colour = colour
self.level = level
self.max_depth = max_depth
self.children = []
def __str__(self) -> str:
"""Return this Block in a string format.
>>> block = Block((0, 0), 750, (1, 128, 181), 0, 1)
>>> str(block)
'Leaf: colour=Pacific Point, pos=(0, 0), size=750, level=0'
"""
if len(self.children) == 0:
indents = '\t' * self.level
colour = colour_name(self.colour)
return f'{indents}Leaf: colour={colour}, pos={self.position}, ' \
f'size={self.size}, level={self.level}'
else:
indents = '\t' * self.level
result = f'{indents}Parent: pos={self.position},' \
f'size={self.size}, level={self.level}'
for child in self.children:
result += f'\n{child}'
return result
def __eq__(self, other: Block) -> bool:
"""Return True iff this Block and all its descendents are equivalent to
the <other> Block and all its descendents.
>>> b1 = Block((0, 0), 750, (0, 0, 0), 0, 1)
>>> b2 = Block((0, 0), 750, (0, 0, 0), 0, 1)
>>> b1 == b2
True
>>> b3 = Block((0, 0), 750, (255, 255, 255), 0, 1)
>>> b1 == b3
False
"""
if len(self.children) == 0 and len(other.children) == 0:
# Both self and other are leaves.
return (self.position == other.position
and self.size == other.size
and self.colour == other.colour
and self.level == other.level
and self.max_depth == other.max_depth)
elif len(self.children) != len(other.children):
# One of self or other is a leaf while the other is not.
return False
else:
# Both self and other have four children.
# Because of RIs, don't need to check any attributes other
# than the children, since will eventually hit base case!
return self.children == other.children # elementwise compare
def child_size(self) -> int:
"""Return the size of this Block's children.
"""
return round(self.size / 2.0)
def children_positions(self) -> list[tuple[int, int]]:
"""Return the (x, y) coordinates of this Block's four children.
The positions are returned in this order: upper-right child, upper-left
child, lower-left child, lower-right child.
"""
x = self.position[0]
y = self.position[1]
size = self.child_size()
return [(x + size, y), (x, y), (x, y + size), (x + size, y + size)]
def _update_children_positions(self, position: tuple[int, int]) -> None:
"""Set the position of this Block to <position> and update all its
descendants to have positions consistent with this Block's position.
<position> is the (x, y) coordinates of the upper-left corner of this
Block.
"""
# TODO: Implement this method
def smashable(self) -> bool:
"""Return True iff this block can be smashed.
A block can be smashed if it has no children and its level is not at
max_depth.
"""
return self.level != self.max_depth and len(self.children) == 0
def smash(self) -> bool:
""" Return True iff the smash was performed successfully.
A smash is successful if the block genrates four children blocks and
has no colour anymore.
Smashing a block requires that the block has no children and that the
block's level is less than the max_depth.
For each new child, there is a chance the child will be smashed as well.
The procedure for determining whether a child will be smashed is as
follows:
- Use function `random.random` to generate a random number in the
interval [0, 1).
- If the random number is less than `math.exp(-0.25 * level)`, where
`level` is the level of this child `Block`, then the child `Block`
will be smashed.
- If the child `Block` is not smashed, uniform randomly assign the child
a color from the list of colours in `settings.COLOUR_LIST`.
If this Block's level is <max_depth>, do nothing. If this block has
children, do nothing.
>>> position = (0, 0)
>>> size = 750
>>> level = 0
>>> max_depth = 1
>>> b1 = Block(position, size, (0, 0, 0), level, max_depth)
>>> b1.smash()
True
>>> b1.position == position
True
>>> b1.size == size
True
>>> b1.level == level
True
>>> b1.colour is None
True
>>> len(b1.children) == 4
True
>>> b1.max_depth == max_depth
True
"""
# create 4 new arbitrary children, (pick at random)
if self.smashable():
self.colour = None
child_pos = self.children_positions()
colours = COLOUR_LIST.copy()
self.children = [
Block(child_pos[0],
self.child_size(),
random.choice(colours),
self.level + 1, self.max_depth),
Block(child_pos[1],
self.child_size(),
random.choice(colours),
self.level + 1, self.max_depth),
Block(child_pos[2],
self.child_size(),
random.choice(colours),
self.level + 1, self.max_depth),
Block(child_pos[3],
self.child_size(),
random.choice(colours),
self.level + 1, self.max_depth)
]
for child in self.children:
if random.random() < math.exp(-0.25 * child.level):
child.smash()
return True
def swap(self, direction: int) -> bool:
"""Swap the child Blocks of this Block.
If this Block has no children, do nothing. Otherwise, if <direction> is
SWAP_VERT, swap vertically.
If <direction> is SWAP_HORZ, swap horizontally.
Return True iff the swap was performed.
Precondition:
- <direction> is either (SWAP_VERT, SWAP_HORZ)
"""
if not self.children:
return False
else:
if direction == 'SWAP_VERT':
self.children[0], self.children[1], \
self.children[2], self.children[3] = \
self.children[3], self.children[2], \
self.children[1], self.children[0]
elif direction == 'SWAP_HORZ':
self.children[0], self.children[1], \
self.children[2], self.children[3] = \
self.children[1], self.children[0], \
self.children[3], self.children[2]
self._update_children_positions(self.position)
return True
def rotate(self, direction: int) -> bool:
"""Rotate this Block and all its descendents.
If this Block has no children, do nothing (no rotation is performed).
If <direction> is ROT_CW, rotate clockwise.
If <direction> is ROT_CCW, rotate counter-clockwise.
Return True iff the rotation was performed.
Preconditions:
- direction in (ROT_CW, ROT_CCW)
"""
if len(self.children) == 0:
return False
else:
if direction == 'ROT_CW':
self.children[0], self.children[1], \
self.children[2], self.children[3] = \
self.children[1], self.children[2], \
self.children[3], self.children[0]
elif direction == 'ROT_CCW':
self.children[0], self.children[1], \
self.children[2], self.children[3] = \
self.children[3], self.children[0], \
self.children[1], self.children[2]
self._update_children_positions(self.position)
for item in self.children:
item.rotate(direction)
return True
def paint(self, colour: tuple[int, int, int]) -> bool:
"""Change this Block's colour iff it is a leaf at a level of max_depth
and its colour is different from <colour>.
Return True iff this Block's colour was changed.
"""
if self.level == self.max_depth and self.colour != colour:
self.colour = colour
return True
else:
return False
def combine(self) -> bool:
"""Turn this Block into a leaf based on the majority colour of its
children. Each child block must also be a leaf.
The majority colour is the colour with the most child blocks of that
colour. A tie does not constitute a majority (e.g., if there are two red
children and two blue children, then there is no majority colour).
The method should do nothing for the following cases:
- If there is no majority colour among the children.
- If the block has no children.
Return True iff this Block was turned into a leaf node.
"""
if self.level != (self.max_depth - 1) or len(self.children) == 0:
# children block are not leaves or no children
return False
else:
majority = None
colours = {}
for col in COLOUR_LIST:
colours[col] = 0
for child in self.children:
if child.colour in colours:
colours[child.colour] += 1
twos = []
for col in colours:
if colours[col] > 2:
majority = col
elif colours[col] == 2:
twos.append(col)
if len(twos) == 1:
majority = twos[0]
elif len(twos) == 2:
majority = None
if majority is None:
return False
else:
self.children = []
self.colour = majority
return True
def create_copy(self) -> Block:
"""Return a new Block that is a deep copy of this Block.
Remember that a deep copy has new blocks (not aliases) at every level.
>>> block = generate_board(3, 750)
>>> copy = block.create_copy()
>>> id(block) != id(copy)
True
>>> block == copy
True
"""
block = Block(self.position, self.size, self.colour, self.level,
self.max_depth)
if len(self.children) > 0:
for child in self.children:
block.children.append(child.create_copy())
return block
if __name__ == '__main__':
import doctest
doctest.testmod()
# This is a board consisting of only one block.
b1 = Block((0, 0), 750, COLOUR_LIST[0], 0, 1)
print("tiny board:")
print(b1)
# Now let's make a random board.
b2 = generate_board(3, 750)
print("\nrandom board:")
print(b2)