Example #1
0
    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
Example #2
0
 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)
Example #3
0
 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)
Example #4
0
 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)
Example #5
0
 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 = []
Example #6
0
 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]
Example #7
0
 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)
Example #8
0
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)