def _create_tree(self): """Create the underlying tree. The algorithm starts with a source in the center and chooses an already visited tile at random to extend the tree to a random unvisited tile. """ self._nodes = GridContainer(Grid(self.columns, self.rows)) nodes = self._nodes # alias for x, y in itertools.product(range(self.columns), range(self.rows)): nodes[x, y] = Builder.Node() visited = GridContainer(Grid(self.columns, self.rows), False) visited[self._source] = True boundary = {self._source} while True: new_boundary = set() moves = [] # [(parent, child, direction), ...] FIXME namedtuple for parent in boundary: for direction in Direction: child = parent + direction.vector if self.wrap: child = Vector2d(child.x % self.columns, child.y % self.rows) if 0 <= child.x < self.columns and 0 <= child.y < self.rows and not visited[ child]: moves.append((parent, child, direction)) new_boundary.add(parent) if not moves: break weights = [] for p, _, d in moves: nodes[p].edges[d] = True link = nodes[p].to_tile().link nodes[p].edges[d] = False weights.append(self.difficulties[self.difficulty][link]) parent, child, direction = weighted_choice(moves, weights) new_boundary.add(child) visited[child] = True nodes[parent].edges[direction] = True nodes[child].edges[-direction] = True boundary = new_boundary # Transform nodes/edges into tiles with link type and orientation tiles = GridContainer(Grid(self.columns, self.rows)) for x, y in itertools.product(range(self.columns), range(self.rows)): tiles[x, y] = nodes[x, y].to_tile() tiles[self._source].entity = EntityType.source return tiles
def clone(self): """Clone (i.e. deep-copy) the state.""" new_grid_container = GridContainer(self.tiles.grid) for node in self.tiles.grid: new_grid_container[node] = Solver.Tile(self.tiles[node].link, self.tiles[node].possible_orientations.copy()) new_edges = self.edges.clone() return Solver.State(new_grid_container, new_edges)
def _setup_board(self): self.board = GridContainer(Grid(self.columns, self.rows)) # for storing widgets self.grid.clear_widgets() # RelativeLayout for x, y in itertools.product(range(self.columns), range(self.rows)): if self.puzzle.grid[x, y].entity == EntityType.source: t = SourceWidget(self.puzzle.grid[x, y], pos=(x * tilesize, y * tilesize)) elif self.puzzle.grid[x, y].entity == EntityType.drain: t = DrainWidget(self.puzzle.grid[x, y], pos=(x * tilesize, y * tilesize)) else: t = TileWidget(self.puzzle.grid[x, y], pos=(x * tilesize, y * tilesize)) t.bind(on_change=self.on_tile_change) self.board[x, y] = t self.grid.add_widget(t)
def _check_power(self, state): """Check if every tile on the grid has power.""" power = GridContainer(self.grid, False) work = {self.source} while work: node = work.pop() power[node] = True for neighbor in self.grid.neighbors(node): if state.edges[node, neighbor] is Edges.State.present and not power[neighbor]: work.add(neighbor) return all(power[node] for node in self.grid)
def __init__(self, puzzle): self.columns = puzzle.grid.columns self.rows = puzzle.grid.rows self.source = puzzle.source self.grid = Grid(puzzle.grid.columns, puzzle.grid.rows) self.tiles = GridContainer(self.grid) for node in self.grid: self.tiles[node] = Solver.Tile(puzzle.grid[node].link) self.edges = Edges(self.columns, self.rows) if not puzzle.wrap: self._handle_boundary() self._handle_walls(puzzle.walls) self._handle_adjacent_drains() self.solutions = []
def check_power(self): """Check which tile is connected to the power source""" power = GridContainer(Grid(self.columns, self.rows), False) work = {self.puzzle.source} while work: parent = work.pop() power[parent] = True for direction in Direction: proto_child = parent + direction.vector if not self.puzzle.wrap and not self.board.grid.valid( proto_child): continue child = Vector2d(proto_child.x % self.columns, proto_child.y % self.rows) # noinspection PyTypeChecker if (not power[child] and self._connected(parent, child, direction) and not self._blocking_wall(parent, proto_child)): work.add(child) for x, y in itertools.product(range(self.columns), range(self.rows)): self.board[x, y].powered = power[x, y]
def example(cls): """A simple example puzzle""" grid = GridContainer(Grid(3, 3)) grid[0, 2] = Tile(LinkType.dead_end, Direction.down) grid[1, 2] = Tile(LinkType.t_intersection, Direction.right) grid[2, 2] = Tile(LinkType.corner, Direction.left) grid[0, 1] = Tile(LinkType.dead_end, Direction.down) grid[1, 1] = Tile(LinkType.t_intersection, Direction.down) grid[1, 1].entity = EntityType.source grid[2, 1] = Tile(LinkType.straight, Direction.right) grid[0, 0] = Tile(LinkType.dead_end, Direction.down) grid[1, 0] = Tile(LinkType.corner, Direction.up) grid[2, 0] = Tile(LinkType.dead_end, Direction.right) walls = { Wall(Vector2d(0, 2), Wall.Orientation.horizontal), Wall(Vector2d(2, 1), Wall.Orientation.vertical) } return Puzzle(grid, walls, source=Vector2d(1, 1), expected_moves=7, wrap=False)
class NetGame(Widget): """Main widget :ivar kivy.properties.ObjectProperty main_window: reference to main widget (without background) :ivar kivy.properties.ObjectProperty grid: reference to the game board widget (`RelativeLayout`) :ivar kivy.properties.ObjectProperty timer: reference to the timer label :ivar kivy.properties.OptionProperty state: game state (possible states are waiting', 'running', 'paused' and 'solved') :ivar kivy.properties.NumericProperty moves: number of moves played :ivar kivy.properties.NumericProperty expected_moves: the expected (close to minimal) number of moves required to solve the puzzle :ivar builder.Puzzle puzzle: the puzzle currently displayed :ivar int columns: number of columns of the game board :ivar int rows: number of rows of the game board :ivar grid.Grid board: the game board, a :class:`grid.Grid` of :class:`TileWidget` objects """ main_window = ObjectProperty() grid = ObjectProperty() timer = ObjectProperty() state = OptionProperty('waiting', options=['waiting', 'running', 'paused', 'solved']) moves = NumericProperty(0) expected_moves = NumericProperty(10) def __init__(self, puzzle, **kwargs): """Create a new widget displaying the given puzzle. Adjusts window size.""" super(NetGame, self).__init__(**kwargs) self.puzzle = puzzle self.columns = puzzle.grid.columns self.rows = puzzle.grid.rows self.grid.width = self.columns * tilesize self.grid.height = self.rows * tilesize self.expected_moves = self.puzzle.expected_moves self.board = None self._last_changed = None # last changed tile self.ids['pause_button'].bind(on_press=self.on_pause) # Set window size to at least size of game dialog Window.size = (max(self.main_window.width, 300), max(self.main_window.height, 350)) Window.bind(on_key_down=self.on_key_down) self._setup_board() self._setup_walls() self.check_power() def _setup_board(self): self.board = GridContainer(Grid(self.columns, self.rows)) # for storing widgets self.grid.clear_widgets() # RelativeLayout for x, y in itertools.product(range(self.columns), range(self.rows)): if self.puzzle.grid[x, y].entity == EntityType.source: t = SourceWidget(self.puzzle.grid[x, y], pos=(x * tilesize, y * tilesize)) elif self.puzzle.grid[x, y].entity == EntityType.drain: t = DrainWidget(self.puzzle.grid[x, y], pos=(x * tilesize, y * tilesize)) else: t = TileWidget(self.puzzle.grid[x, y], pos=(x * tilesize, y * tilesize)) t.bind(on_change=self.on_tile_change) self.board[x, y] = t self.grid.add_widget(t) def _setup_walls(self): for w in self.puzzle.walls: self.grid.add_widget(WallWidget(w)) # Double the walls on the board's borders if w.position.x == 0 and w.orientation == Wall.Orientation.vertical: w2 = Wall(Vector2d(w.position.x + self.columns, w.position.y), w.orientation) # noinspection PyTypeChecker self.grid.add_widget(WallWidget(w2)) if w.position.y == 0 and w.orientation == Wall.Orientation.horizontal: w2 = Wall(Vector2d(w.position.x, w.position.y + self.rows), w.orientation) # noinspection PyTypeChecker self.grid.add_widget(WallWidget(w2)) def on_pause(self, instance): """Handle pressing the pause button""" del instance # unused parameter self.state = 'paused' self.timer.stop() def proceed(self): """Unpause the game""" if self._last_changed is None: self.state = 'waiting' else: self.state = 'running' self.timer.start() def reset(self): """Reset the puzzle to its original state""" # Unlock all tiles and set their rotational angle back to the original state for x, y in itertools.product(range(self.columns), range(self.rows)): self.board[x, y].powered = False self.board[x, y].locked = False self.board[x, y].angle = self.puzzle.grid[x, y].orientation.angle self.board[x, y].orientation = self.puzzle.grid[x, y].orientation.angle self.check_power() self.moves = 0 self.timer.stop() self.timer.reset() self._last_changed = None self.state = 'waiting' def on_key_down(self, window, key, scancode, codepoint, modifiers): del window, scancode, codepoint, modifiers # unused parameters if self.state in ('waiting', 'running') and key == Keyboard.keycodes['escape']: self.on_pause(self) return True def on_tile_change(self, tile): """Slot called when a tile is rotated""" if self.state == 'waiting': self.timer.start() self.state = 'running' if self._last_changed is not tile: self._last_changed = tile self.moves += 1 self.check_power() solved = self._check_solved() if solved: self.timer.stop() self.state = 'solved' def check_power(self): """Check which tile is connected to the power source""" power = GridContainer(Grid(self.columns, self.rows), False) work = {self.puzzle.source} while work: parent = work.pop() power[parent] = True for direction in Direction: proto_child = parent + direction.vector if not self.puzzle.wrap and not self.board.grid.valid( proto_child): continue child = Vector2d(proto_child.x % self.columns, proto_child.y % self.rows) # noinspection PyTypeChecker if (not power[child] and self._connected(parent, child, direction) and not self._blocking_wall(parent, proto_child)): work.add(child) for x, y in itertools.product(range(self.columns), range(self.rows)): self.board[x, y].powered = power[x, y] def _connected(self, parent, child, direction): """Check if both parent and child tile have arms in opposite directions""" parent_angle = direction.angle child_angle = (parent_angle + 180) % 360 return self.board[parent].has_arm( parent_angle) and self.board[child].has_arm(child_angle) def _blocking_wall(self, parent, child): """Check if parent and child are separated by a wall""" orientation = Wall.Orientation.horizontal if parent.x == child.x else Wall.Orientation.vertical position = Vector2d(max(parent.x, child.x), max(parent.y, child.y)) position = Vector2d(position.x % self.columns, position.y % self.rows) return Wall(position, orientation) in self.puzzle.walls def _check_solved(self): """Check if all tiles are connected to the power source""" return all(tile.powered for tile in self.board.items()) def score(self) -> int: """Calculate the score""" if not self.state == 'solved': return 0 weights = { LinkType.empty: 0, LinkType.dead_end: 4, LinkType.corner: 4, LinkType.straight: 2, LinkType.t_intersection: 4, LinkType.cross_intersection: 0 } score = 0 for x, y in itertools.product(range(self.columns), range(self.rows)): score += weights[self.puzzle.grid[x, y].link] score -= 2 * len( self.puzzle.walls) # wall: -1 point for left and right square if not self.puzzle.wrap: # implicit walls score -= self.puzzle.grid.columns + self.puzzle.grid.rows score *= self.puzzle.expected_moves / (self.puzzle.grid.columns * self.puzzle.grid.rows) score = score**2 / self.timer.seconds return round(score)