Tiles
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import math | |
from scene import run, Scene, LabelNode, SpriteNode, Size, Point, Texture, Action | |
import secrets | |
from enum import Enum | |
class Direction(tuple, Enum): | |
UP = (-1, 0) # 1 row up, no column change | |
DOWN = (1, 0) # 1 row down, no column change | |
LEFT = (0, -1) # no row change, one column left | |
RIGHT = (0, 1) # no row change, one column right | |
@classmethod | |
def from_radians(cls, radians): | |
"""Converts radians angle to `Direction`. | |
Args: | |
radians (float): Angle in radians where 0 points up | |
""" | |
degrees = math.degrees(radians) | |
if degrees >= 45 and degrees < 135: | |
return Direction.RIGHT | |
elif degrees >= 135 and degrees < 215: | |
return Direction.DOWN | |
elif degrees >= 215 and degrees < 305: | |
return Direction.LEFT | |
else: | |
return Direction.UP | |
class Model: | |
""" | |
(0, 0) is top left corner. | |
Args: | |
tile_count (int): Number of tiles per row & column | |
Attributes: | |
_positions (dict): Map where key is tile index and value is tuple (row, col) | |
_empty_position (tuple): Tuple holding empty position (row, col) | |
""" | |
def __init__(self, tile_count): | |
self.tile_count = tile_count | |
available_positions = [ | |
(r, c) | |
for r in range(tile_count) | |
for c in range(tile_count) | |
] | |
self._positions = {} | |
for i in range(tile_count * tile_count - 1): | |
position = secrets.choice(available_positions) | |
self._positions[i] = position | |
available_positions.remove(position) | |
self._empty_position = available_positions[0] | |
def tile_position(self, index): | |
return self._positions[index] | |
def moved_tile_position(self, index, direction): | |
position = self.tile_position(index) | |
new_position = (position[0] + direction[0], position[1] + direction[1]) | |
if not new_position == self._empty_position: | |
raise ValueError(f'Unable to move tile {index} to {direction}') | |
return new_position | |
def move_tile(self, index, direction): | |
new_position = self.moved_tile_position(index, direction) | |
old_position = self._positions[index] | |
self._positions[index] = new_position | |
self._empty_position = old_position | |
return new_position | |
class TileNode(SpriteNode): | |
_TEXTURE = { | |
True: 'spc:PowerupYellow', | |
False: 'spc:PowerupBlue' | |
} | |
def __init__(self, index, *args, **kwargs): | |
super().__init__(self._TEXTURE[False], *args, **kwargs) | |
self.index = index | |
self.title_node = LabelNode(f'{index+1}', font=('Helvetica-Bold', 24), position=(0, 1), parent=self) | |
self._highlighted = False | |
@property | |
def highlighted(self): | |
return self._highlighted | |
@highlighted.setter | |
def highlighted(self, x): | |
# We have to save size, because texture change resets it | |
size = self.size | |
self.texture = Texture(self._TEXTURE[x]) | |
self.size = size | |
self._highlighted = x | |
class GameScene(Scene): | |
""" | |
Args: | |
tile_count (int): Number of tiles per row / column | |
tile_margin (float): Spacing around each tile | |
board_size_ratio (float): How big is the board, 1.0 whole width / height, 0.8 = 80% | |
""" | |
def __init__(self, tile_count, tile_margin, board_size_ratio, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
self.tile_count = tile_count | |
self.tile_margin = tile_margin | |
self.board_size_ratio = board_size_ratio | |
self.model = Model(tile_count) | |
# Nodes | |
self.board_background = None | |
self.tiles = None | |
# Cached values for moving tile | |
self._active_tile_index = None | |
self._touch_began_location = None | |
# Cached value, invalidated when did_change_size is called | |
self._tile_size = None | |
self._board_background_size = None | |
self._board_background_position = None | |
self._top_left_tile_position = None | |
def setup(self): | |
self.background_color = 'black' | |
self.board_background = SpriteNode('spc:BackgroundDarkPurple') | |
self.add_child(self.board_background) | |
self.tiles = [] | |
for i in range(self.tile_count * self.tile_count - 1): | |
node = TileNode(i) | |
self.add_child(node) | |
self.tiles.append(node) | |
self.did_change_size() | |
def invalidate_cached_values(self): | |
self._tile_size = None | |
self._board_background_size = None | |
self._board_background_position = None | |
self._top_left_tile_position = None | |
@property | |
def board_background_size(self): | |
if self._board_background_size is None: | |
min_size = min(self.size.width, self.size.height) | |
board_size = self.board_size_ratio * min_size | |
self._board_background_size = Size(board_size, board_size) | |
return self._board_background_size | |
@property | |
def board_background_position(self): | |
if self._board_background_position is None: | |
self._board_background_position = self.size / 2 | |
return self._board_background_position | |
@property | |
def tile_size(self): | |
if self._tile_size is None: | |
size = (self.board_background_size.width - self.tile_margin * (self.tile_count + 1)) / self.tile_count | |
self._tile_size = Size(size, size) | |
return self._tile_size | |
@property | |
def top_left_tile_position(self): | |
if self._top_left_tile_position is None: | |
x = self.board_background_position.x - self.board_background_size.width / 2 | |
x += self.tile_margin + self.tile_size.width / 2 | |
y = self.board_background_position.y + self.board_background_size.height / 2 | |
y -= self.tile_margin + self.tile_size.height / 2 | |
self._top_left_tile_position = Point(x, y) | |
return self._top_left_tile_position | |
def tile_position(self, row, col): | |
top_left = self.top_left_tile_position | |
size = self.tile_size.width | |
x = top_left.x + col * (size + self.tile_margin) | |
y = top_left.y - row * (size + self.tile_margin) | |
return Point(x, y) | |
def did_change_size(self): | |
self.invalidate_cached_values() | |
self.board_background.size = self.board_background_size | |
self.board_background.position = self.board_background_position | |
for i in range(self.tile_count * self.tile_count - 1): | |
position = self.model.tile_position(i) | |
tile = self.tiles[i] | |
tile.size = self.tile_size | |
tile.position = self.tile_position(*position) | |
def touch_began(self, touch): | |
if self._active_tile_index is not None: | |
return | |
for tile in self.tiles: | |
if touch.location in tile.frame: | |
self._active_tile_index = tile.index | |
self._touch_began_location = touch.location | |
tile.highlighted = True | |
return | |
def _move_direction(self, touch): | |
theta = math.atan2( | |
touch.location.x - self._touch_began_location.x, | |
touch.location.y - self._touch_began_location.y, | |
) | |
if theta < 0.0: | |
theta += math.pi * 2 | |
return Direction.from_radians(theta) | |
def touch_moved(self, touch): | |
if self._active_tile_index is None: | |
return | |
if touch.location == self._touch_began_location: | |
return | |
direction = self._move_direction(touch) | |
tile = self.tiles[self._active_tile_index] | |
try: | |
new_position = self.model.moved_tile_position(tile.index, direction) | |
except ValueError: | |
new_position = self.model.tile_position(tile.index) | |
tile.run_action(Action.move_to(*self.tile_position(*new_position), 0.2)) | |
def touch_ended(self, touch): | |
if self._active_tile_index is None: | |
return | |
direction = self._move_direction(touch) | |
tile = self.tiles[self._active_tile_index] | |
try: | |
new_position = self.model.move_tile(tile.index, direction) | |
except ValueError: | |
new_position = self.model.tile_position(tile.index) | |
tile.run_action(Action.move_to(*self.tile_position(*new_position), 0.2)) | |
tile.highlighted = False | |
self._active_tile_index = None | |
def main(): | |
run(GameScene(4, 20, 0.8)) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment