Skip to content

Instantly share code, notes, and snippets.

@zrzka
Last active February 2, 2018 11:00
Embed
What would you like to do?
Tiles
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