Skip to content

Commit

Permalink
Merge pull request #23 from GamesCrafters/dev
Browse files Browse the repository at this point in the history
Release 0.3.0
  • Loading branch information
Ant1ng2 authored May 3, 2020
2 parents 6a09ffe + accef24 commit 9bf7af5
Show file tree
Hide file tree
Showing 47 changed files with 1,631 additions and 349 deletions.
7 changes: 7 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[report]
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover

# Don't complain if tests don't hit defensive assertion code:
raise NotImplementedError
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ celerybeat-schedule
.venv
env/
venv/
*venv/
ENV/
env.bak/
venv.bak/
Expand All @@ -126,4 +127,7 @@ dmypy.json

# Databases
*.pkl
database*
database*

# Temp
temp*
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@ python -m puzzlesolver.puzzles.hanoi
```
to play a puzzle of Towers of Hanoi

Run from the base directory of the respository
```
python -m puzzlesolver.server
```
to access the webserver. The server should be running at http://127.0.0.1:9001/puzzles/.

## Exploring GamesmanPuzzles
Tips for exploring this repository:
1. [Follow the guides and learn how to create a puzzle and a solver!](guides)
2. Definitely explore the [puzzlesolver](puzzlesolver) in depth. There should be a README.md in every important directory to explain what each file does.
2. Definitely explore the [puzzlesolver](puzzlesolver) in depth.
3. Understand what a [puzzle tree](https://nyc.cs.berkeley.edu/wiki/Puzzle_tree) is.

## Contributing to GamesmanPuzzles
Expand Down
14 changes: 14 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# configuration related to pull request comments
comment: no # do not comment PR with the result

coverage:
range: 70..80 # coverage lower than 50 is red, higher than 90 green, between color code

status:
project: # settings affecting project coverage
default:
target: auto # auto % coverage target
threshold: 5% # allow for 5% reduction of coverage without failing

# do not run coverage on patch nor changes
patch: false
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ from puzzlesolver import PuzzlePlayer

The rest of guide will implement the instance methods of this class.

[Next step: Gameplay Methods](1_Gameplay_Methods.md)
[Next step: Gameplay Methods](01_Gameplay_Methods.md)
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ def primitive(self, **kwargs):
return PuzzleValue.SOLVABLE
return PuzzleValue.UNDECIDED
```
[Next step: Implementing the Move functions](2_Moves.md)
[Next step: Implementing the Move functions](02_Moves.md)
2 changes: 1 addition & 1 deletion guides/tutorial/2_Moves.md → guides/tutorial/02_Moves.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,4 @@ python <your_python_file_name>.py
```
If everything runs smoothly, congrats! You have created a playable puzzle!

[Next step: Implementing the Solver methods](3_Solver_Methods.md)
[Next step: Implementing the Solver methods](03_Solver_Methods.md)
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ Now that we have our own puzzle, it's time to solve it with our GeneralSolver! U

## Solver functions
#### ```__hash__(self)```
This hash function allows the solver to use memoization to store previously computed values so that the solver doesn't require any other new computation. Hashes also must be unique such that no two "different" puzzle states have the same hash. Note that two unequal puzzle states do not necessarily have to be "different", as one puzzle state can simply be a variation (i.e. rotation, reflection) of the other state. This fact allows us to further optimize our solver to remove any redundant puzzle states.
This hash function allows the solver to use memoization to store previously computed values so that the solver doesn't require any other new computation. Hashes also must be unique such that no two "different" puzzle states have the same hash. Note that two unequal puzzle states do not necessarily have to be "different", as one puzzle state can simply be a variation (i.e. rotation, reflection) of the other state, or equivalently, **both puzzles have the same remoteness**. This fact allows us to further optimize our solver to remove any redundant puzzle states.

Our Hanoi puzzle will not be optimizing over redundant states and will simply just use the hash of the string representation of our stacks.
```python
def __hash__(self):
return hash(str(self.stacks))
from hashlib import sha1
h = sha1()
h.update(str(self.stacks).encode())
return int(h.hexdigest(), 16)
```

#### ```generateSolutions(self, **kwargs):```
Expand All @@ -28,7 +31,8 @@ def generateSolutions(self, **kwargs):
### Execute
Once you have implemented all the required functions, change the last line of the Python file outside of the class to:
```python
PuzzlePlayer(Hanoi(), solver=GeneralSolver()).play()
puzzle = Hanoi()
PuzzlePlayer(puzzle, solver=GeneralSolver(puzzle)).play()
```
On your CLI, execute
```bash
Expand All @@ -39,6 +43,6 @@ If you have a remoteness of 7 and a Primitive value of "UNDECIDED", congrats! Yo
## Extras
Ponder on these questions in how we can optimize this puzzle
- If we change our endstate to be a stack on either the middle or right rod, how can we optimize this?
- Why is deserializing a hash to a puzzle a bad idea?
- If you can compute a hash that directly encodes the remoteness, is there a need for a solver?

[Next Part: Implementing a Solver](4_Solver_Prerequisites.md)
[Next Part: Implementing a Solver](04_Solver_Prerequisites.md)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Solver Prerequisites
This next part of the tutorial will teach you how to make a custom solver following the Solver interface. We'll be implementing the BFS algorithm GeneralSolver. This guide will assume that you are already familiar with Python 3 and that you have checked out the following documentation for a [puzzle tree](https://nyc.cs.berkeley.edu/wiki/Puzzle_tree). This guide also assumes that you've followed the prerequisites of creating a Puzzle.
This next part of the tutorial will teach you how to make a custom solver following the Solver interface. We'll be implementing the in memory BFS algorithm GeneralSolver. This guide will assume that you are already familiar with Python 3 and that you have checked out the following documentation for a [puzzle tree](https://nyc.cs.berkeley.edu/wiki/Puzzle_tree). This guide also assumes that you've followed the prerequisites of creating a Puzzle.

## Initialize files
Create a **NEW** Python file and import the following:
Expand All @@ -13,4 +13,4 @@ import queue as q

The rest of guide will implement the instance methods of this class.

[Next step: Helper Methods](5_Helper_Methods.md)
[Next step: Helper Methods](05_Helper_Methods.md)
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class GeneralSolver(Solver):

## Implementing Functions

**```__init__(self, **kwargs)```**
**```__init__(self, puzzle, **kwargs)```**

We initialize the dictionaries used to store the values and remoteness of the positions.
```python
Expand Down Expand Up @@ -40,4 +40,4 @@ def getValue(self, puzzle, **kwargs):
```
Note that in the official `GeneralSolver`, there is no method for `getValue`, however it is defined in the `Solver` class. This is because `getValue` only relies on the function `getRemoteness` and doesn't require any other solver attributes, meaning we can make `getValue` be part of the abstraction.

[Next Step: Implementing the Solver Methods](6_Solver_Methods.md)
[Next Step: Implementing the Solver Methods](06_Solver_Methods.md)
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ The `solve` function is the core of all solvers in the GamesmanPuzzles and is us

Our GeneralSolver traverses the puzzle tree using the solve function. First, start with the function initalization:
```python
def solve(self, **kwargs)
def solve(self, puzzle, **kwargs):
```

Remember back in the puzzle project, we defined a few important functions that were meant to be used for this solver. These functions are:
Expand All @@ -24,24 +24,21 @@ Following the steps of the algorithm we defined in [puzzle tree:](https://nyc.cs

Splitting the algorithm into two separate parts:

Step 1: (the ```helper``` function would be defined in Step 2, 3, & 4)
Step 1: Initializations of the winstates (solutions) of the puzzle as well as setting their remotenesses to 0. Additionally also initalizing the Queue data structure used for BFS.
```python
def solve(self, **kwargs):
# continued...
ends = self.puzzle.generateSolutions()
for end in ends:
self.remoteness[hash(end)] = 0
helper(self, ends)
solutions, queue = self.puzzle.generateSolutions(), q.Queue()
for solution in solutions:
self.remoteness[hash(solution)] = 0
queue.put(solution)
```

Step 2, 3, & 4:
Step 2, 3, & 4: The following performs the BFS portion of the algorithm.
```python
def helper(self, puzzles):
queue = q.Queue()
for puzzle in puzzles: queue.put(puzzle)
# BFS for remoteness classification
while not queue.empty():
puzzle = queue.get()
for move in puzzle.generateMoves(movetype="undo"):
for move in puzzle.generateMoves('undo'):
nextPuzzle = puzzle.doMove(move)
if hash(nextPuzzle) not in self.remoteness:
self.remoteness[hash(nextPuzzle)] = self.remoteness[hash(puzzle)] + 1
Expand All @@ -50,37 +47,42 @@ def helper(self, puzzles):

The final solve function should look like this:
```python
def solve(self, **kwargs):
# BFS for remoteness classification
def helper(self, puzzles):
queue = q.Queue()
for puzzle in puzzles: queue.put(puzzle)
while not queue.empty():
puzzle = queue.get()
for move in puzzle.generateMoves():
nextPuzzle = puzzle.doMove(move)
if hash(nextPuzzle) not in self.remoteness:
self.remoteness[hash(nextPuzzle)] = self.remoteness[hash(puzzle)] + 1
queue.put(nextPuzzle)
def solve(self, *args, **kwargs):
"""Traverse the entire puzzle tree and classifiers all the
positions with values and remoteness
- If position already exists in memory, returns its value
"""
solutions, queue = self.puzzle.generateSolutions(), q.Queue()
for solution in solutions:
self.remoteness[hash(solution)] = 0
queue.put(solution)

ends = self.puzzle.generateSolutions()
for end in ends:
self.remoteness[hash(end)] = 0
helper(self, ends)
# BFS for remoteness classification
while not queue.empty():
puzzle = queue.get()
for move in puzzle.generateMoves('undo'):
nextPuzzle = puzzle.doMove(move)
if hash(nextPuzzle) not in self.remoteness:
self.remoteness[hash(nextPuzzle)] = self.remoteness[hash(puzzle)] + 1
queue.put(nextPuzzle)
```

### Execute
Once you have implemented all the required functions, change the last line of the Python file outside of the class to:
```python
PuzzlePlayer(Hanoi(), solver=GeneralSolver(Hanoi())).play()
puzzle = Hanoi()
PuzzlePlayer(puzzle, solver=GeneralSolver(puzzle)).play()
```
On your CLI, execute
```bash
python <your_python_file_name>.py
```
If you have a Solver value of SOLVABLE, congrats! You have successfully implemented a Solver!
If you have a Solver value of SOLVABLE, congrats! You have successfully implemented a BFS Solver!

## Extras
Ponder on these questions in how we can optimize this solver
- Try thinking of a way to parallelize this solver.
- Try thinking of a way to optimize the algorithm for this solver.
- Are there any ways we can solve this not through BFS?
- This solver will not save the positions it classifies after execution is over. Think about ways we can add persistance to our solvers.

[Next Step: 7 Server Introduction](07_Server_Introduction.md)
26 changes: 26 additions & 0 deletions guides/tutorial/07_Server_Introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Server Introduction
GamesmanPuzzles provides a Web API to display values of puzzles. This guide will adapt our puzzle from the previous steps into a format which can be displayed onto the Web API.

You can manually run the server by running
```python
python -m puzzlesolver.server
```
and accessing [http://localhost:9001](http://localhost:9001). You can explore the Web API using the documentation (to do soon).

## Prerequisites

After this guide, you should able to display a puzzle in a test application.

Begin by importing all the extra dependencies in our Hanoi puzzle that we made before
```python
from puzzlesolver.puzzles import ServerPuzzle
```

Change the class we inherit from Puzzle to ServerPuzzle

```python
class Hanoi(ServerPuzzle):
```

The next steps would be implementing extra functions:
[Next Step: PuzzleID](08_PuzzleID.md)
13 changes: 13 additions & 0 deletions guides/tutorial/08_PuzzleID.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Puzzle ID
```
GET puzzles/<puzzle_id>
```
The first part of our url is the identification of our puzzle. This is a class variable and can be used to access the variants (next section).

Define it inside your class:
```python
class Hanoi(ServerPuzzle):
puzzleid = 'hanoi'
```

[Next Step: Variants](09_Variants.md)
53 changes: 53 additions & 0 deletions guides/tutorial/09_Variants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Variants Introduction

```
GET puzzles/<puzzle_id>/<variant_id>
```

Sometimes we want our puzzle to support multiple versions of itself. For example, in Hanoi we can support puzzles with more than just three discs. To support this, we introduce **Variants**.

## Variants
A **variant** is a modified version of a puzzle (i.e. more pieces, different orientation). Each puzzle variant is independent of each other, defined to be that there is no position in one variant that can exist in another variant.

Each initialized puzzle will have a different variant, so we need to modify the `__init__()` and `generateSolutions()` functions to support different variant (size) numbers.

#### **`__init__()`**
```python
def __init__(self, size=3, **kwargs):
self.stacks = [
list(range(size, 0, -1)),
[],
[]
]
```

#### **`generateSolutions()`**
```python
def generateSolutions(self, **kwargs):
newPuzzle = Hanoi(size=int(self.variant))
newPuzzle.stacks = [
[],
[],
list(range(int(self.variant), 0, -1))
]
return [newPuzzle]
```

We also need a property to get the current variant of the puzzle. (`@property` is a tag that we use to defined the function as a property of the function, such as `Hanoi().variant`)
```python
@property
def variant(self):
size = 0
for stack in self.stacks:
size += len(stack)
return str(size)
```
Our server requires that we define a dictionary with variant ids as the keys as well as different Solver classes as the values. *Note: The reason for this is that different variants may use different solvers.*

Just for these purposes, lets consider only variants up to 3 as well as using the GeneralSolver as the main solver.
```python
variants = {
str(i) : GeneralSolver for i in range(1, 4)
}
```
[Next Step: Positions](10_Positions.md)
Loading

0 comments on commit 9bf7af5

Please sign in to comment.