sparc-puzzle 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.3
2
+ Name: sparc-puzzle
3
+ Version: 0.1.0
4
+ Summary:
5
+ Author: larskaesberg
6
+ Author-email: larskaesberg@gmail.com
7
+ Requires-Python: >=3.10
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: datasets (>=3.6.0,<4.0.0)
14
+ Requires-Dist: openai (>=1.86.0,<2.0.0)
15
+ Description-Content-Type: text/markdown
16
+
17
+ <p align="center">
18
+ <img src="logo.png" alt="SPaRC Logo" width="128"/>
19
+ </p>
20
+
21
+ # SPaRC Dataset
22
+
23
+ **SPaRC: A Spatial Pathfinding and Reasoning Challenge**
24
+
25
+ <div align="center">
26
+ <a href="https://sparc.gipplab.org/" style="margin-right:1em; text-decoration:none; font-size:1.1em;">
27
+ <strong>🌐 Visit the Website</strong>
28
+ </a>
29
+ </div>
30
+
31
+ ## Overview
32
+
33
+ This central repository aggregates all SPaRC-related projects and provides quick access to the visualization front-end, solver utilities, and puzzle generator. Whether you're looking to run the web app, extend solver functionality, or create new puzzles, start here!
34
+
35
+ ## Repositories
36
+
37
+ * **Website (Visualizer Front-End)**
38
+
39
+ * [SPaRCWebsite](https://github.com/lkaesberg/SPaRCWebsite)
40
+ * **Solver**
41
+
42
+ * [SPaRCSolver](https://github.com/lkaesberg/SPaRCSolver)
43
+ * **Puzzle Generator**
44
+
45
+ * [SPaRCPuzzleGenerator](https://github.com/lkaesberg/SPaRCPuzzleGenerator)
46
+
47
+ ## Live Demo & Dataset
48
+
49
+ Interact with SPaRC puzzles and explore the visualization and dataset online:
50
+
51
+ * [🔗 Live Demo & App](https://sparc.gipplab.org/)
52
+ * [📚 SPaRC Dataset on Hugging Face](https://huggingface.co/datasets/lkaesberg/SPaRC)
53
+
54
+ ## Contributing
55
+
56
+ Contributions, issues, and feature requests are welcome! Please open an issue or submit a pull request in the relevant repository.
57
+
@@ -0,0 +1,40 @@
1
+ <p align="center">
2
+ <img src="logo.png" alt="SPaRC Logo" width="128"/>
3
+ </p>
4
+
5
+ # SPaRC Dataset
6
+
7
+ **SPaRC: A Spatial Pathfinding and Reasoning Challenge**
8
+
9
+ <div align="center">
10
+ <a href="https://sparc.gipplab.org/" style="margin-right:1em; text-decoration:none; font-size:1.1em;">
11
+ <strong>🌐 Visit the Website</strong>
12
+ </a>
13
+ </div>
14
+
15
+ ## Overview
16
+
17
+ This central repository aggregates all SPaRC-related projects and provides quick access to the visualization front-end, solver utilities, and puzzle generator. Whether you're looking to run the web app, extend solver functionality, or create new puzzles, start here!
18
+
19
+ ## Repositories
20
+
21
+ * **Website (Visualizer Front-End)**
22
+
23
+ * [SPaRCWebsite](https://github.com/lkaesberg/SPaRCWebsite)
24
+ * **Solver**
25
+
26
+ * [SPaRCSolver](https://github.com/lkaesberg/SPaRCSolver)
27
+ * **Puzzle Generator**
28
+
29
+ * [SPaRCPuzzleGenerator](https://github.com/lkaesberg/SPaRCPuzzleGenerator)
30
+
31
+ ## Live Demo & Dataset
32
+
33
+ Interact with SPaRC puzzles and explore the visualization and dataset online:
34
+
35
+ * [🔗 Live Demo & App](https://sparc.gipplab.org/)
36
+ * [📚 SPaRC Dataset on Hugging Face](https://huggingface.co/datasets/lkaesberg/SPaRC)
37
+
38
+ ## Contributing
39
+
40
+ Contributions, issues, and feature requests are welcome! Please open an issue or submit a pull request in the relevant repository.
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "sparc-puzzle"
3
+ version = "0.1.0"
4
+ description = ""
5
+ authors = [
6
+ {name = "larskaesberg",email = "larskaesberg@gmail.com"}
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "datasets (>=3.6.0,<4.0.0)",
12
+ "openai (>=1.86.0,<2.0.0)"
13
+ ]
14
+
15
+ [project.scripts]
16
+ sparc = "sparc.sparc:main"
17
+
18
+ [tool.poetry]
19
+ name = "sparc-puzzle"
20
+ version = "0.1.0"
21
+ description = ""
22
+ authors = ["larskaesberg <larskaesberg@gmail.com>"]
23
+ readme = "README.md"
24
+ packages = [{include = "sparc"}]
25
+
26
+ [tool.poetry.dependencies]
27
+ python = "^3.10"
28
+
29
+ [build-system]
30
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
31
+ build-backend = "poetry.core.masonry.api"
File without changes
@@ -0,0 +1,137 @@
1
+ from typing import Dict
2
+ import json
3
+
4
+
5
+ def prompt(puzzle_data: Dict) -> str:
6
+ grid_size = puzzle_data.get("grid_size", {"width": 0, "height": 0})
7
+ puzzle_array = puzzle_data.get("puzzle_array", [])
8
+ grid_str = "\n".join(map(str, puzzle_array))
9
+ start_pos = None
10
+ end_pos = None
11
+ for y, row in enumerate(puzzle_array):
12
+ for x, cell in enumerate(row):
13
+ if cell == "S":
14
+ start_pos = f"({x}, {y})"
15
+ elif cell == "E":
16
+ end_pos = f"({x}, {y})"
17
+
18
+ polyshapes_str = ""
19
+ if "polyshapes" in puzzle_data and puzzle_data["polyshapes"]:
20
+ polyshapes_str = "POLYSHAPES DEFINITIONS:\n"
21
+ polyshapes_json = json.loads(puzzle_data["polyshapes"])
22
+ for shape_id, shape_def in polyshapes_json.items():
23
+ polyshapes_str += f"Shape {shape_id}:\n{"\n".join(map(str, shape_def))}\n\n"
24
+ return f"""
25
+ ## Objective
26
+ You are a specialized AI proficient in spatial reasoning and solving puzzles from the game 'The Witness'. Your goal is to find a valid path (a continuous line) from the specified Start Node to the End Node on the provided grid, adhering to all puzzle rules.
27
+
28
+ ## Core Concepts & Grid Basics
29
+ * **Grid Dimensions:** The puzzle grid has {grid_size['width']} columns and {grid_size['height']} rows.
30
+ * **Coordinate System:** Nodes are identified by `(x, y)` coordinates. `(0,0)` is the top-left node. `x` increases to the right, `y` increases downwards.
31
+ * **Path:** The solution is a single, continuous line connecting adjacent nodes either horizontally or vertically.
32
+ * **No Revisits:** The path **CANNOT** visit the same node more than once.
33
+ * **Valid Path Cells:** The path travels along the grid lines (edges between nodes). It can only occupy positions marked `+` or `.` in the grid layout (these correspond to positions with at least one even coordinate).
34
+ * **Rule Cells:** Cells containing rule symbols (squares, stars, etc.) have coordinates where both `x` and `y` are odd. The path goes *around* these rule cells, never *on* them.
35
+ * **Regions:** The drawn path divides the grid cells into one or more distinct enclosed areas (regions). Many rules apply based on the contents of these regions.
36
+
37
+ ## Symbol Legend (Grid Notation)
38
+ * `S`: **Start Node** (Path begins here)
39
+ * `E`: **End Node** (Path ends here)
40
+ * `+`: Valid cell for the path to occupy
41
+ * `N`: Empty rule cell (no rule)
42
+ * `G`: **Gap** (Path **CANNOT** cross this cell)
43
+ * `.`: **Dot** (Path **MUST** pass through this cell)
44
+ * `o-X`: **Square** of color X
45
+ * `*-X`: **Star** of color X
46
+ * `A-X`: **Triangle** (touch 1 edge)
47
+ * `B-X`: **Triangle** (touch 2 edges)
48
+ * `C-X`: **Triangle** (touch 3 edges)
49
+ * `D-X`: **Triangle** (touch 4 edges)
50
+ * `P-X-Y`: **Polyshape** (positive) of color X and shape ID Y
51
+ * `Y-X-Y`: **Negative Polyshape** (ylop) of color X and shape ID Y
52
+
53
+ **Color Codes:** R=Red, B=Blue, G=Green, Y=Yellow, W=White, O=Orange, P=Purple, K=Black
54
+
55
+ ## Detailed Solving Rules
56
+ The drawn path must satisfy **ALL** applicable constraints:
57
+
58
+ 1. **Path Constraints:**
59
+ * Path **MUST** start at `S` and end at `E`.
60
+ * Path connects adjacent nodes (horizontal/vertical moves only).
61
+ * Nodes **CANNOT** be revisited.
62
+ * Path **MUST** pass through all Dot (`.`) cells.
63
+ * Path **CANNOT** pass through any Gap (`G`) cells.
64
+
65
+ 2. **Region-Based Rules** (Apply to areas enclosed by the path):
66
+ * **Squares (`o-X`):** All squares within a single region **MUST** be the same color. Squares of different colors **MUST** be separated into different regions by the path.
67
+ * **Stars (`*-X`):** Within a single region, each star symbol **MUST** be paired with exactly **ONE** other element (star or square) *of the same color*. Other colors within the region are irrelevant to this specific star's rule.
68
+ * **Polyshapes (`P-X-Y`):** The region containing this symbol **MUST** be able to contain the specified shape (defined in Polyshape Definitions). The shape must fit entirely within the region's boundaries. If multiple positive polyshapes are in one region, the region must accommodate their combined, non-overlapping forms. Rotation of polyshapes is generally allowed unless context implies otherwise.
69
+ * **Negative Polyshapes (`Y-X-Y`):** These "subtract" shape requirements, typically within the same region as corresponding positive polyshapes. A negative polyshape cancels out a positive polyshape of the exact same shape and color within that region. If all positive shapes are canceled, the region has no shape constraint. A negative shape is only considered 'used' if it cancels a positive one. Negative shapes can sometimes rationalize apparent overlaps or boundary violations of positive shapes if interpreted as cancellations.
70
+
71
+ 3. **Path-Based Rules (Edge Touching):**
72
+ * **Triangles (`A-X`, `B-X`, `C-X`, `D-X`):** The path **MUST** touch a specific number of edges of the cell containing the triangle symbol.
73
+ * `A-X` (1): Path touches **EXACTLY 1** edge of the triangle's cell.
74
+ * `B-X` (2): Path touches **EXACTLY 2** edges of the triangle's cell.
75
+ * `C-X` (3): Path touches **EXACTLY 3** edges of the triangle's cell.
76
+ * `D-X` (4): Path touches **EXACTLY 4** edges (fully surrounds) the triangle's cell.
77
+
78
+ ## EXAMPLE PUZZLE GRID:
79
+
80
+ ["+",".","+","+","+","E","+"]
81
+ ["+","C-R","+","o-K","+","o-K","+"]
82
+ ["S","+","+","+","+","+","+"]
83
+ ["+","P-G-112","+","*-G","+","P-B-624","+"]
84
+ ["+","+","+","+","+","+","+"]
85
+ ["+","*-G","+","*-G","+","o-K","+"]
86
+ ["+","+","+",".","+","+","+"]
87
+
88
+ EXAMPLE POLYSHAPE DEFINITIONS:
89
+ Shape 112:
90
+ [0,1,0,0]
91
+ [0,1,0,0]
92
+ [0,1,0,0]
93
+ [0,0,0,0]
94
+
95
+ Shape 624:
96
+ [0,1,0,0]
97
+ [0,1,1,0]
98
+ [0,1,0,0]
99
+ [0,0,0,0]
100
+
101
+ EXAMPLE SOLUTION:
102
+
103
+ We start at (0,2) and draw a line to (0,0).
104
+ We then draw a line to (2,0) to reach the dot at (1,0) and surround the 3 count triangle.
105
+ We then draw a line to (2,2) here we go down to touch the third side of the triangle cell and therefore validate the 3 count triangle.
106
+ We continue down to (2,6) to validate the polyshape 112 and also the green star with the green polyshape
107
+ After this we draw a line to (4,6) to start validating the polyshape 624 by surrounding it.
108
+ Therefore we have to draw a line to (6,4) over (4,4) which creates a region for the stone at (5,5) which validates the stone.
109
+ We continue up to (6,2) for the polyshape 624 and then go to (4,2) and after this to (4,0) to finaly validate the polyshape 624.
110
+ This also validates the two green stars at (3,3) and (3,5) with each other and the black stone at (3,1) because its the only stone in its region.
111
+ This line also creates a region for the black stone at (5,1) because its the only stone in its region.
112
+ Now we can draw a line to (5,0) to reach the end node.
113
+
114
+ #### (0,2),(0,1),(0,0),(1,0),(2,0),(2,1),(2,2),(2,3),(2,4),(2,5),(2,6),(3,6),(4,6),(4,5),(4,4),(5,4),(6,4),(6,3),(6,2),(5,2),(4,2),(4,1),(4,0),(5,0)
115
+
116
+ ## Puzzle Input Data
117
+ * **Start Node:** {start_pos}
118
+ * **End Node:** {end_pos}
119
+ * **Grid Layout:**
120
+ ```
121
+ {grid_str}
122
+ ```
123
+ * **Polyshape Definitions (if applicable):**
124
+ * Shapes are defined by 2D arrays where '1' indicates an occupied cell and '0' indicates an empty cell.
125
+ ```
126
+ {polyshapes_str}
127
+ ```
128
+
129
+ ## Task & Output Format
130
+ 1. **Solve the Puzzle:** Determine the valid path from the Start Node to the End Node that satisfies all rules.
131
+ 2. **Explain Reasoning:** Provide a step-by-step explanation of your thought process. Detail key deductions, how constraints were applied, and any backtracking or choices made.
132
+ 3. **Provide Solution Path:** After the reasoning, output the exact marker string `####` followed immediately by the solution path as a list of node coordinates `(x, y)`. Include all intermediate nodes from start to end.
133
+
134
+ **Example Solution Path Format:**
135
+ ####
136
+ [(0, 0), (1, 0), (2, 0), (2, 1), ...]
137
+ """
@@ -0,0 +1,38 @@
1
+ from typing import Dict
2
+ from sparc.prompt import prompt
3
+ from sparc.validation import extract_solution_path, validate_solution, analyze_path
4
+ from datasets import load_dataset
5
+ from openai import OpenAI
6
+
7
+
8
+ def main() -> None:
9
+ dataset = load_dataset("lkaesberg/SPaRC", "all", split="test")
10
+
11
+ client = OpenAI(
12
+ api_key="9c89c616-649e-4d77-a6ad-1b1e525f94b5",
13
+ base_url="http://services.gipplab.org:8080/v1",
14
+ )
15
+ response = client.chat.completions.create(
16
+ model="meta-llama/Llama-3.3-70B-Instruct",
17
+ messages=[
18
+ {
19
+ "role": "system",
20
+ "content": "You are an expert at solving puzzles games.",
21
+ },
22
+ {"role": "user", "content": prompt(dataset[0])},
23
+ ],
24
+ temperature=0.7,
25
+ )
26
+ message = response.choices[0].message.content
27
+ extracted_path = extract_solution_path(message, dataset[0])
28
+
29
+ solved = validate_solution(extracted_path, dataset[0])
30
+ analysis = analyze_path(extracted_path, dataset[0])
31
+
32
+ print(f"Extracted Path: {extracted_path}")
33
+ print(f"Solved: {solved}")
34
+ print(f"Analysis: {analysis}")
35
+
36
+
37
+ if __name__ == "__main__":
38
+ main()
@@ -0,0 +1,193 @@
1
+ import re
2
+ from typing import List, Optional, Dict
3
+
4
+
5
+ def extract_solution_path(
6
+ solution_text: str, puzzle_data: Dict = None
7
+ ) -> Optional[List[Dict[str, int]]]:
8
+ """Extract solution path from LLM's response
9
+
10
+ Args:
11
+ solution_text: Text response from the LLM
12
+ puzzle_data: Optional puzzle data dict to extract end point from
13
+
14
+ Returns:
15
+ List of coordinate dicts or None if no path found
16
+ """
17
+ # First, check if "Solution" appears in the text
18
+ solution_marker = "####"
19
+ if solution_marker in solution_text:
20
+ # Only process text after "####"
21
+ solution_part = solution_text.split(solution_marker, 1)[-1]
22
+ else:
23
+ # If no solution marker, use the full text
24
+ solution_part = solution_text
25
+
26
+ # Look for coordinate patterns like (0,0) -> (0,1) or similar
27
+ # Pattern for (x,y) or (x, y) coordinates
28
+ coord_pattern = r"\((\d+),\s*(\d+)\)"
29
+ coords = re.findall(coord_pattern, solution_part)
30
+
31
+ if coords:
32
+ # Extract end point from puzzle data if provided
33
+ end_point = None
34
+ if puzzle_data:
35
+ puzzle_array = puzzle_data.get("puzzle_array", [])
36
+ for y, row in enumerate(puzzle_array):
37
+ for x, cell in enumerate(row):
38
+ if cell == "E":
39
+ end_point = {"x": x, "y": y}
40
+ break
41
+ if end_point:
42
+ break
43
+
44
+ # Convert string coordinates to integer dicts
45
+ path = []
46
+ for x, y in coords:
47
+ point = {"x": int(x), "y": int(y)}
48
+ path.append(point)
49
+
50
+ # Stop extracting if we've reached the end point
51
+ if (
52
+ end_point
53
+ and point["x"] == end_point["x"]
54
+ and point["y"] == end_point["y"]
55
+ ):
56
+ break
57
+
58
+ return path
59
+
60
+ # If no coordinates found, return None
61
+ return None
62
+
63
+
64
+ def validate_solution(extracted_path: List[Dict[str, int]], puzzle_data: Dict) -> bool:
65
+ """Validate if the solution is valid by comparing with known solutions
66
+
67
+ Args:
68
+ extracted_path: List of coordinate dicts
69
+ puzzle_data: Dictionary containing puzzle data including solutions
70
+
71
+ Returns:
72
+ Boolean indicating if the solution is valid
73
+ """
74
+ extracted_path = [(p["x"], p["y"]) for p in extracted_path]
75
+ if not extracted_path or len(extracted_path) < 2:
76
+ return False
77
+
78
+ # Check against all valid solutions in the database
79
+ all_solutions = puzzle_data.get("solutions", [])
80
+ if not all_solutions:
81
+ return False
82
+
83
+ # For each solution in the database, check if our path matches
84
+ for solution in all_solutions:
85
+ solution_path = [(p["x"], p["y"]) for p in solution["path"]]
86
+
87
+ # Check if the paths match exactly
88
+ if extracted_path == solution_path:
89
+ return True
90
+
91
+ return False
92
+
93
+
94
+ def analyze_path(solution_path: List[Dict[str, int]], puzzle: Dict) -> Dict:
95
+ """Analyze the solution path for detailed validation metrics
96
+
97
+ Args:
98
+ solution_path: List of coordinate dicts representing the solution path
99
+ puzzle: Puzzle dictionary with puzzle array and metadata
100
+
101
+ Returns:
102
+ Dictionary with detailed path analysis results
103
+ """
104
+ solution_path = [(p["x"], p["y"]) for p in solution_path]
105
+ if not solution_path:
106
+ return {
107
+ "starts_at_start_ends_at_exit": False,
108
+ "connected_line": False,
109
+ "non_intersecting_line": False,
110
+ "start_to_exit_connected": False,
111
+ "no_rule_crossing": False,
112
+ "fully_valid_path": False,
113
+ }
114
+
115
+ # Get puzzle information
116
+ puzzle_array = puzzle.get("puzzle_array", [])
117
+ if not puzzle_array:
118
+ return {
119
+ "starts_at_start_ends_at_exit": False,
120
+ "connected_line": False,
121
+ "non_intersecting_line": False,
122
+ "start_to_exit_connected": False,
123
+ "no_rule_crossing": False,
124
+ "fully_valid_path": False,
125
+ }
126
+
127
+ # Find start and end points
128
+ start_point = None
129
+ end_point = None
130
+ for y, row in enumerate(puzzle_array):
131
+ for x, cell in enumerate(row):
132
+ if cell == "S":
133
+ start_point = (x, y)
134
+ elif cell == "E":
135
+ end_point = (x, y)
136
+
137
+ if not start_point or not end_point:
138
+ return {
139
+ "starts_at_start_ends_at_exit": False,
140
+ "connected_line": False,
141
+ "non_intersecting_line": False,
142
+ "start_to_exit_connected": False,
143
+ "no_rule_crossing": False,
144
+ "fully_valid_path": False,
145
+ }
146
+
147
+ # Check if path starts at start and ends at exit
148
+ starts_at_start = len(solution_path) > 0 and solution_path[0] == start_point
149
+ ends_at_exit = len(solution_path) > 0 and solution_path[-1] == end_point
150
+ starts_at_start_ends_at_exit = starts_at_start and ends_at_exit
151
+
152
+ # Check if path is connected (no gaps)
153
+ connected_line = True
154
+ for i in range(1, len(solution_path)):
155
+ prev_x, prev_y = solution_path[i - 1]
156
+ curr_x, curr_y = solution_path[i]
157
+ # Check if adjacent (Manhattan distance of 1)
158
+ if abs(prev_x - curr_x) + abs(prev_y - curr_y) != 1:
159
+ connected_line = False
160
+ break
161
+
162
+ # Check if path doesn't intersect with itself
163
+ non_intersecting_line = len(set(solution_path)) == len(solution_path)
164
+
165
+ # Check if there's a connected path from start to exit
166
+ start_to_exit_connected = starts_at_start_ends_at_exit and connected_line
167
+
168
+ # Identify rule cells as cells where both coordinates are odd
169
+ rule_cells = set()
170
+ for y in range(len(puzzle_array)):
171
+ for x in range(len(puzzle_array[0]) if len(puzzle_array) > 0 else 0):
172
+ if x % 2 == 1 and y % 2 == 1:
173
+ rule_cells.add((x, y))
174
+
175
+ # Check if path crosses rule cells
176
+ no_rule_crossing = not any((x, y) in rule_cells for x, y in solution_path[1:-1])
177
+
178
+ # Check if path is fully valid
179
+ fully_valid_path = (
180
+ starts_at_start_ends_at_exit
181
+ and connected_line
182
+ and non_intersecting_line
183
+ and no_rule_crossing
184
+ )
185
+
186
+ return {
187
+ "starts_at_start_ends_at_exit": starts_at_start_ends_at_exit,
188
+ "connected_line": connected_line,
189
+ "non_intersecting_line": non_intersecting_line,
190
+ "start_to_exit_connected": start_to_exit_connected,
191
+ "no_rule_crossing": no_rule_crossing,
192
+ "fully_valid_path": fully_valid_path,
193
+ }