🕳️ _Matrix

GitHub stars GitHub forks GitHub issues Dor Pascal

Welcome to the Matrix Project

Project Overview

Project _Matrix (Line-Matrix) is a comprehensive exploration of AI search algorithms, using the 8-Puzzle problem as a case study. The program includes implementations of famous search algorithms, such as BFS, IDDFS, GBFS, and A* search algorithms, and a custom heuristic for A* and GBFS. The program was initially developed for the 20551 Introduction to Artificial Intelligence course at the Open University of Israel, and earned a perfect score of 100/100.

AI8Puzzle Animation

AI8Puzzle GTK GUI (C++)

Gallery

AI8Puzzle GTK GUI AI8Puzzle Theme AI8Puzzle Theme

theMatrix theme GUI

The 8-Puzzle Problem

The 8-Puzzle problem is a classic AI problem, where the goal is to move the tiles from the initial state to the target state, using the minimum number of moves. The puzzle consists of a 3x3 grid with 8 numbered tiles and one empty space. The tiles are initially arranged in a random order, and the goal is to arrange them in ascending order, with the empty space at the bottom right corner, or at the top left corner (see wikipedia).

8-Puzzle Moving Tile

Example of 8-Puzzle: Moving tile 1 from the top left corner, then moving 4 to the middle

States

class State:
    def __init__(self, numbers: List[int], rows=3, cols=3):
        self.numbers = numbers
        self.rows, self.cols = rows, cols
        self.matrix = self._create_matrix(numbers, rows, cols)

    def _create_matrix(self, numbers, rows, cols):
        return [[numbers[i * cols + j] for j in range(cols)] for i in range(rows)]

The State class is responsible for representing a state in the world. A state, in this specific problem, is a configuration of the puzzle. Each puzzle configuration converts a linear array of tiles into a 2D matrix. This design choice simplifies both the visualization of the puzzle state and the implementation of moves within the puzzle space.

Initial State

Shuffled 8-Puzzle

Example of shuffled 8-Puzzle, GUI with cubes theme

def main():
    try:
        initial_numbers = [int(num) for num in sys.argv[3:]]
        initial_state = State(initial_numbers)
    except ValueError:
        print("All arguments must be integers.")
        sys.exit(1)

    print(BFS(initial_state))
    print(IDDFS(initial_state))
    print(GBFS(initial_state))
    print(AStar(initial_state))

The initial state, the one without a parent attribute, is defined based on the user input and starts as the tree root. The main function is responsible for parsing and initiating the initial_state instance, then it prints the result.

Actions and Transition Model

8-Puzzle Transition Model

Clicking the colored tiles will move them to the empty space

def generate_children(self):
            blank_x, blank_y = self.find_number_in_matrix(0)
            children = []
            for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
                x, y = blank_x + dx, blank_y + dy
                if 0 <= x < self.rows and 0 <= y < self.cols:
                    new_numbers = self.numbers.copy()
                    blank_index, new_index = blank_x * self.cols + blank_y, x * self.cols + y
                    new_numbers[blank_index], new_numbers[new_index] = new_numbers[new_index], new_numbers[blank_index]
                    children.append(State(new_numbers))
            return children
        

The actions and transition model are implemented inside the State class's generate_children method. The State class also includes methods for generating children states, considering possible moves from the current state. This method implements the Actions and the transition model.

Goal States

8-Puzzle Goal State

Path to goal state of the 8-Puzzle

The problem includes two goal states, defined as plain List[int] to match the program initial_state input format.

TARGET_A = [0, 1, 2, 3, 4, 5, 6, 7, 8]
TARGET_B = [1, 2, 3, 4, 5, 6, 7, 8, 0]

Action Cost

Each action performed by generate_children above costs 1, so 1 is the action cost of the problem. The cost is managed in the Node class which will be described below.

Running the Program

The program supports both interactive GUI and command-line interfaces. Two GUI versions are available: one implemented in Python using the tkinter library, and another implemented in C++ using the GTK library. The Python version is easier to use and has more features, while the C++ version offers cool visual effects and supports CSS styling.

To run the C++ GUI, first make sure you have GTK installed. Then navigate to the directory containing AI8Puzzle and execute the following command in the terminal:

./make && ./main -s number_of_tiles

The -s flag is optional and allows you to set the number of tiles in the puzzle (including the blank tile). The default is 9 tiles. The program will generate a random solvable puzzle and open a window where you can interact with the puzzle.

Elegant Theme GUI

Elegant theme GUI

You can move the tiles by clicking on them, or use the keyboard arrow keys. You can also use the following shortcuts:

Menu (C++)

Menu (C++)

To run the Python GUI, first make sure you have tkinter installed. Then navigate to the directory containing Tiles.py and execute the following command in the terminal:

python3 GUI.py
AI8Puzzle Solution 7 Moves AI8Puzzle Solution 7 Moves

To run the program in the command-line interface mode, navigate to the directory containing Tiles.py and execute the following command in the terminal:

python3 Tiles.py 9_space_separated_numbers_representing_the_initial_state

The program will print the results for each algorithm in the required format:

<Algorithm name>
<Explored count - number of expanded nodes>
<Solution Path>

Technical Details

class SearchAlgorithm:
    class ExploredSet:
        def __init__(self, initial_state, name=None):
            self.root_node = Node(initial_state)
            self.frontier = None
            self.explored = self.ExploredSet()
            self.name = name
            self.solution_node = None

The SearchAlgorithm class and its nested ExploredSet class, along with other relevant details of the technical implementation, are included in the source code of the project. For more details, refer to the project's documentation and source code.

Node Class

The node class is responsible for representing a node in the search tree. The program uses two types of nodes: a base Node class and a derived PriorityQueueNode class. Each Node instance stores the current state, the parent node, and the path cost g(n). In addition, the PriorityQueueNode class, used in A* and GBFS, includes a priority attribute for the heuristic value h(n).

class PriorityQueueNode(Node):
            def __init__(self, node):
                super().__init__(node.state, node.parent)
                self.priority = None

            def __lt__(self, other):
                if self.priority is None or other.priority is None:
                    raise ValueError("Cannot compare nodes with undefined priority")
                return self.priority < other.priority
        

Search Data Structures

The Frontier class acts as the fringe or the boundary between explored and unexplored states. It is a priority queue (minimum heap) in A* and GBFS, and a simple queue or stack in BFS and IDDFS, respectively. In BFS it's implemented as a FIFO queue, while in IDDFS as a stack.

class GBFS(SearchAlgorithm):
    class GBFSFrontier(Frontier):
        def set_priority(self, node):
            return node.my_heuristic()

The GBFSFrontier sets only the heuristic to the priority, thus f(n) = h(n).

class AStar(SearchAlgorithm):
    class AStarFrontier(Frontier):
        def set_priority(self, node):
            return node.cost + node.my_heuristic()

The AStarFrontier adds the node cost to its priority, thus f(n) = h(n) + g(n).

Custom A* and GBFS Heuristics

The program includes custom heuristics for A* and GBFS. The heuristics are implemented as methods inside the PriorityQueueNode class and are used to calculate the estimated cost from the current state to the target state. The heuristics are used to guide the search algorithms in finding the optimal path to the target state.

Console Output

Output Example Output Example 2

Acknowledgments

Inspired by the book "Artificial Intelligence: A Modern Approach" by Stuart Russell and Peter Norvig. Thanks to the Open University of Israel for the opportunity to work on this project.

License

This project is licensed under the MIT License - see the LICENSE file for details.