From dbfd564ccf6504db0af36b82fe1acea2ed2adb50 Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Mon, 18 Feb 2019 14:20:02 -0500 Subject: [PATCH 01/17] Added the recursion counter to hanoi.py --- .pylintrc | 2 ++ .vscode/settings.json | 3 +++ Chapter1/hanoi.py | 7 ++++++- requirements.txt | 1 + 4 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .pylintrc create mode 100644 .vscode/settings.json create mode 100644 requirements.txt diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..bdb2eba --- /dev/null +++ b/.pylintrc @@ -0,0 +1,2 @@ +disable=print-statement, + unsubscriptable-object diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..84c8f83 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "venv/bin/python3" +} \ No newline at end of file diff --git a/Chapter1/hanoi.py b/Chapter1/hanoi.py index 48ab600..64ebaf2 100644 --- a/Chapter1/hanoi.py +++ b/Chapter1/hanoi.py @@ -16,6 +16,9 @@ from typing import TypeVar, Generic, List T = TypeVar('T') +# Globals +calls: int = 0 +num_discs: int = 3 class Stack(Generic[T]): @@ -24,6 +27,8 @@ def __init__(self) -> None: def push(self, item: T) -> None: self._container.append(item) + global calls + calls += 1 def pop(self) -> T: return self._container.pop() @@ -32,7 +37,6 @@ def __repr__(self) -> str: return repr(self._container) -num_discs: int = 3 tower_a: Stack[int] = Stack() tower_b: Stack[int] = Stack() tower_c: Stack[int] = Stack() @@ -54,3 +58,4 @@ def hanoi(begin: Stack[int], end: Stack[int], temp: Stack[int], n: int) -> None: print(tower_a) print(tower_b) print(tower_c) + print("calls: {}".format(calls - num_discs)) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4a2718b --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pylint==2.2.2 From 126e7a9d426f8d3aba257996f0ecfda86fb551b6 Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Sat, 16 Mar 2019 18:56:37 -0400 Subject: [PATCH 02/17] Added hanoi without type hints --- Chapter1/hanoi2.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 Chapter1/hanoi2.py diff --git a/Chapter1/hanoi2.py b/Chapter1/hanoi2.py new file mode 100644 index 0000000..b2ce71e --- /dev/null +++ b/Chapter1/hanoi2.py @@ -0,0 +1,40 @@ +# Globals +num_discs = 3 + +class Stack: + + def __init__(self): + self._container = [] + + def push(self, item): + self._container.append(item) + + def pop(self): + return self._container.pop() + + def __repr__(self): + return repr(self._container) + + +tower_a = Stack() +tower_b = Stack() +tower_c = Stack() + +for i in range(1, num_discs + 1): + tower_a.push(i) + + +def hanoi(begin, end, temp, n): + if n == 1: + end.push(begin.pop()) + else: + hanoi(begin, temp, end, n - 1) + hanoi(begin, end, temp, 1) + hanoi(temp, end, begin, n - 1) + + +if __name__ == "__main__": + hanoi(tower_a, tower_c, tower_b, num_discs) + print(tower_a) + print(tower_b) + print(tower_c) From 0d0f2052f303fdb4766a633d43d20051b058055c Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Sun, 17 Mar 2019 23:27:02 -0400 Subject: [PATCH 03/17] added typing_extensions for Protocol --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 4a2718b..d2adfa7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ pylint==2.2.2 +typing-extensions==3.7.2 From f8a33bd38021157dcb7330a9b20668c067f63d5f Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Thu, 28 Mar 2019 23:41:58 -0400 Subject: [PATCH 04/17] minor changes --- .gitignore | 2 ++ Chapter2/generic_search.py | 4 ++-- Chapter2/maze.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 894a44c..f4e6946 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.idea + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/Chapter2/generic_search.py b/Chapter2/generic_search.py index 6e0ceb7..d8cf667 100644 --- a/Chapter2/generic_search.py +++ b/Chapter2/generic_search.py @@ -126,7 +126,7 @@ def node_to_path(node: Node[T]) -> List[T]: class Queue(Generic[T]): def __init__(self) -> None: - self._container: Deque[T] = Deque() + self._container: Deque[T] = Deque[T]() @property def empty(self) -> bool: @@ -210,4 +210,4 @@ def astar(initial: T, goal_test: Callable[[T], bool], successors: Callable[[T], if __name__ == "__main__": print(linear_contains([1, 5, 15, 15, 15, 15, 20], 5)) # True print(binary_contains(["a", "d", "e", "f", "z"], "f")) # True - print(binary_contains(["john", "mark", "ronald", "sarah"], "sheila")) # False \ No newline at end of file + print(binary_contains(["john", "mark", "ronald", "sarah"], "sheila")) # False diff --git a/Chapter2/maze.py b/Chapter2/maze.py index 6283ba1..458e11f 100644 --- a/Chapter2/maze.py +++ b/Chapter2/maze.py @@ -21,7 +21,7 @@ class Cell(str, Enum): - EMPTY = " " + EMPTY = "." BLOCKED = "X" START = "S" GOAL = "G" From 8dab49af7d337500de6514548e129ff5ed3be35d Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Sun, 28 Apr 2019 13:52:53 -0400 Subject: [PATCH 05/17] mypy dependency --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index d2adfa7..1535df6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pylint==2.2.2 typing-extensions==3.7.2 +mypy==0.701 \ No newline at end of file From 8fd3b028ddf2482a9441e90e39c003101fd9aa7f Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Mon, 29 Apr 2019 23:08:00 -0400 Subject: [PATCH 06/17] added A* search with euclidean heuristic --- Chapter2/maze.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Chapter2/maze.py b/Chapter2/maze.py index 458e11f..7988fe3 100644 --- a/Chapter2/maze.py +++ b/Chapter2/maze.py @@ -126,7 +126,8 @@ def distance(ml: MazeLocation) -> float: m.mark(path2) print(m) m.clear(path2) - # Test A* + + # Test A* manhattan distance: Callable[[MazeLocation], float] = manhattan_distance(m.goal) solution3: Optional[Node[MazeLocation]] = astar(m.start, m.goal_test, m.successors, distance) if solution3 is None: @@ -134,4 +135,16 @@ def distance(ml: MazeLocation) -> float: else: path3: List[MazeLocation] = node_to_path(solution3) m.mark(path3) - print(m) \ No newline at end of file + print(m) + m.clear(path3) + + # Test A* eucledian + distance: Callable[[MazeLocation], float] = euclidean_distance(m.goal) + solution4: Optional[Node[MazeLocation]] = astar(m.start, m.goal_test, m.successors, distance) + if solution4 is None: + print("No solution found using A*!") + else: + path4: List[MazeLocation] = node_to_path(solution4) + m.mark(path4) + print(m) + m.clear(path4) \ No newline at end of file From 79c7a05bd9869ed6b395e700f2e2fe01deec2e31 Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Tue, 30 Apr 2019 00:43:02 -0400 Subject: [PATCH 07/17] added hash and eq to missionaries --- Chapter2/missionaries.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Chapter2/missionaries.py b/Chapter2/missionaries.py index 92a5692..aa38a23 100644 --- a/Chapter2/missionaries.py +++ b/Chapter2/missionaries.py @@ -34,6 +34,12 @@ def __str__(self) -> str: "The boat is on the {} bank.")\ .format(self.wm, self.wc, self.em, self.ec, ("west" if self.boat else "east")) + def __hash__(self) -> int: + return hash((self.wm, self.wc, self.em, self.ec, self.boat)) + + def __eq__(self, other: MCState): + return hash(self) == hash(other) + def goal_test(self) -> bool: return self.is_legal and self.em == MAX_NUM and self.ec == MAX_NUM From 25ae03eb8d4f0f001274bccd9dad242e6700eb6a Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Tue, 30 Apr 2019 22:03:03 -0400 Subject: [PATCH 08/17] refactoring --- Chapter2/missionaries.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Chapter2/missionaries.py b/Chapter2/missionaries.py index aa38a23..6bcfd78 100644 --- a/Chapter2/missionaries.py +++ b/Chapter2/missionaries.py @@ -16,6 +16,7 @@ from __future__ import annotations from typing import List, Optional from generic_search import bfs, Node, node_to_path +from functools import reduce MAX_NUM: int = 3 @@ -38,7 +39,7 @@ def __hash__(self) -> int: return hash((self.wm, self.wc, self.em, self.ec, self.boat)) def __eq__(self, other: MCState): - return hash(self) == hash(other) + return all([getattr(self, el) == getattr(other, el) for el in ["wm", "wc", "em", "ec", "boat"]]) def goal_test(self) -> bool: return self.is_legal and self.em == MAX_NUM and self.ec == MAX_NUM From a4a5ff9bd22866014401e53b2ef42babd6cb12cf Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Sat, 21 Dec 2019 22:36:30 -0500 Subject: [PATCH 09/17] disabled the requirements.txt file --- requirements.txt | 3 --- requirements_txt.bckp | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 requirements.txt create mode 100644 requirements_txt.bckp diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 1535df6..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -pylint==2.2.2 -typing-extensions==3.7.2 -mypy==0.701 \ No newline at end of file diff --git a/requirements_txt.bckp b/requirements_txt.bckp new file mode 100644 index 0000000..0f3ace2 --- /dev/null +++ b/requirements_txt.bckp @@ -0,0 +1,3 @@ +pylint +typing-extensions +mypy \ No newline at end of file From a72f15fa4252ffd8793aa1a45c6c4528d8598bd6 Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Sat, 21 Dec 2019 22:48:35 -0500 Subject: [PATCH 10/17] minor --- Chapter1/hanoi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Chapter1/hanoi.py b/Chapter1/hanoi.py index 64ebaf2..8c7e165 100644 --- a/Chapter1/hanoi.py +++ b/Chapter1/hanoi.py @@ -20,6 +20,7 @@ calls: int = 0 num_discs: int = 3 + class Stack(Generic[T]): def __init__(self) -> None: From a0ab35b75ecf918857c282c08bdfef5a37c6c312 Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Wed, 25 Dec 2019 12:49:22 -0500 Subject: [PATCH 11/17] :art: Ch2 -- cosmetic changes --- Chapter2/generic_search.py | 36 +++++++++++----------------- Chapter2/maze.py | 49 ++++++++++++++++++++------------------ Chapter2/missionaries.py | 12 ++++++---- 3 files changed, 47 insertions(+), 50 deletions(-) diff --git a/Chapter2/generic_search.py b/Chapter2/generic_search.py index d8cf667..78930e4 100644 --- a/Chapter2/generic_search.py +++ b/Chapter2/generic_search.py @@ -14,8 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations -from typing import TypeVar, Iterable, Sequence, Generic, List, Callable, Set, Deque, Dict, Any, Optional -from typing_extensions import Protocol + +from abc import abstractmethod +from typing import TypeVar, Iterable, Sequence, Generic, List, Callable, Set, Deque, Dict, Any, Optional, Protocol from heapq import heappush, heappop T = TypeVar('T') @@ -32,9 +33,11 @@ def linear_contains(iterable: Iterable[T], key: T) -> bool: class Comparable(Protocol): + @abstractmethod def __eq__(self, other: Any) -> bool: ... + @abstractmethod def __lt__(self: C, other: C) -> bool: ... @@ -94,16 +97,20 @@ def __lt__(self, other: Node) -> bool: def dfs(initial: T, goal_test: Callable[[T], bool], successors: Callable[[T], List[T]]) -> Optional[Node[T]]: # frontier is where we've yet to go frontier: Stack[Node[T]] = Stack() + return perform_search(frontier, goal_test, initial, successors) + + +def perform_search(frontier, goal_test, initial, successors): frontier.push(Node(initial, None)) # explored is where we've been explored: Set[T] = {initial} - # keep going while there is more to explore while not frontier.empty: current_node: Node[T] = frontier.pop() current_state: T = current_node.state # if we found the goal, we're done if goal_test(current_state): + print("Explored: {}".format(len(explored))) return current_node # check where we can go next and haven't explored for child in successors(current_state): @@ -145,24 +152,7 @@ def __repr__(self) -> str: def bfs(initial: T, goal_test: Callable[[T], bool], successors: Callable[[T], List[T]]) -> Optional[Node[T]]: # frontier is where we've yet to go frontier: Queue[Node[T]] = Queue() - frontier.push(Node(initial, None)) - # explored is where we've been - explored: Set[T] = {initial} - - # keep going while there is more to explore - while not frontier.empty: - current_node: Node[T] = frontier.pop() - current_state: T = current_node.state - # if we found the goal, we're done - if goal_test(current_state): - return current_node - # check where we can go next and haven't explored - for child in successors(current_state): - if child in explored: # skip children we already explored - continue - explored.add(child) - frontier.push(Node(child, current_node)) - return None # went through everything and never found goal + return perform_search(frontier, goal_test, initial, successors) class PriorityQueue(Generic[T]): @@ -183,7 +173,8 @@ def __repr__(self) -> str: return repr(self._container) -def astar(initial: T, goal_test: Callable[[T], bool], successors: Callable[[T], List[T]], heuristic: Callable[[T], float]) -> Optional[Node[T]]: +def astar(initial: T, goal_test: Callable[[T], bool], successors: Callable[[T], List[T]], + heuristic: Callable[[T], float]) -> Optional[Node[T]]: # frontier is where we've yet to go frontier: PriorityQueue[Node[T]] = PriorityQueue() frontier.push(Node(initial, None, 0.0, heuristic(initial))) @@ -196,6 +187,7 @@ def astar(initial: T, goal_test: Callable[[T], bool], successors: Callable[[T], current_state: T = current_node.state # if we found the goal, we're done if goal_test(current_state): + print("Explored [astar]: {}".format(len(explored))) return current_node # check where we can go next and haven't explored for child in successors(current_state): diff --git a/Chapter2/maze.py b/Chapter2/maze.py index 7988fe3..ff9ace2 100644 --- a/Chapter2/maze.py +++ b/Chapter2/maze.py @@ -20,7 +20,7 @@ from generic_search import dfs, bfs, node_to_path, astar, Node -class Cell(str, Enum): +class Cell(Enum): EMPTY = "." BLOCKED = "X" START = "S" @@ -34,14 +34,15 @@ class MazeLocation(NamedTuple): class Maze: - def __init__(self, rows: int = 10, columns: int = 10, sparseness: float = 0.2, start: MazeLocation = MazeLocation(0, 0), goal: MazeLocation = MazeLocation(9, 9)) -> None: + def __init__(self, rows: int = 10, columns: int = 10, sparseness: float = 0.2, + start: MazeLocation = MazeLocation(0, 0), goal: MazeLocation = MazeLocation(9, 9)) -> None: # initialize basic instance variables self._rows: int = rows self._columns: int = columns self.start: MazeLocation = start self.goal: MazeLocation = goal # fill the grid with empty cells - self._grid: List[List[Cell]] = [[Cell.EMPTY for c in range(columns)] for r in range(rows)] + self._grid: List[List[Cell]] = [[Cell.EMPTY for _c in range(columns)] for _r in range(rows)] # populate the grid with blocked cells self._randomly_fill(rows, columns, sparseness) # fill the start and goal locations in @@ -66,14 +67,14 @@ def goal_test(self, ml: MazeLocation) -> bool: def successors(self, ml: MazeLocation) -> List[MazeLocation]: locations: List[MazeLocation] = [] - if ml.row + 1 < self._rows and self._grid[ml.row + 1][ml.column] != Cell.BLOCKED: - locations.append(MazeLocation(ml.row + 1, ml.column)) if ml.row - 1 >= 0 and self._grid[ml.row - 1][ml.column] != Cell.BLOCKED: locations.append(MazeLocation(ml.row - 1, ml.column)) - if ml.column + 1 < self._columns and self._grid[ml.row][ml.column + 1] != Cell.BLOCKED: - locations.append(MazeLocation(ml.row, ml.column + 1)) if ml.column - 1 >= 0 and self._grid[ml.row][ml.column - 1] != Cell.BLOCKED: locations.append(MazeLocation(ml.row, ml.column - 1)) + if ml.row + 1 < self._rows and self._grid[ml.row + 1][ml.column] != Cell.BLOCKED: + locations.append(MazeLocation(ml.row + 1, ml.column)) + if ml.column + 1 < self._columns and self._grid[ml.row][ml.column + 1] != Cell.BLOCKED: + locations.append(MazeLocation(ml.row, ml.column + 1)) return locations def mark(self, path: List[MazeLocation]): @@ -81,7 +82,7 @@ def mark(self, path: List[MazeLocation]): self._grid[maze_location.row][maze_location.column] = Cell.PATH self._grid[self.start.row][self.start.column] = Cell.START self._grid[self.goal.row][self.goal.column] = Cell.GOAL - + def clear(self, path: List[MazeLocation]): for maze_location in path: self._grid[maze_location.row][maze_location.column] = Cell.EMPTY @@ -90,19 +91,21 @@ def clear(self, path: List[MazeLocation]): def euclidean_distance(goal: MazeLocation) -> Callable[[MazeLocation], float]: - def distance(ml: MazeLocation) -> float: - xdist: int = ml.column - goal.column - ydist: int = ml.row - goal.row - return sqrt((xdist * xdist) + (ydist * ydist)) - return distance + def _distance(ml: MazeLocation) -> float: + x_dist: int = ml.column - goal.column + y_dist: int = ml.row - goal.row + return sqrt(x_dist * x_dist + y_dist * y_dist) + + return _distance def manhattan_distance(goal: MazeLocation) -> Callable[[MazeLocation], float]: - def distance(ml: MazeLocation) -> float: - xdist: int = abs(ml.column - goal.column) - ydist: int = abs(ml.row - goal.row) - return (xdist + ydist) - return distance + def _distance(ml: MazeLocation) -> float: + x_dist: int = abs(ml.column - goal.column) + y_dist: int = abs(ml.row - goal.row) + return x_dist + y_dist + + return _distance if __name__ == "__main__": @@ -131,20 +134,20 @@ def distance(ml: MazeLocation) -> float: distance: Callable[[MazeLocation], float] = manhattan_distance(m.goal) solution3: Optional[Node[MazeLocation]] = astar(m.start, m.goal_test, m.successors, distance) if solution3 is None: - print("No solution found using A*!") + print("No solution found using A* [manhattan]!") else: path3: List[MazeLocation] = node_to_path(solution3) m.mark(path3) print(m) m.clear(path3) - # Test A* eucledian - distance: Callable[[MazeLocation], float] = euclidean_distance(m.goal) + # Test A* euclidean + distance = euclidean_distance(m.goal) solution4: Optional[Node[MazeLocation]] = astar(m.start, m.goal_test, m.successors, distance) if solution4 is None: - print("No solution found using A*!") + print("No solution found using A* [euclidean]!") else: path4: List[MazeLocation] = node_to_path(solution4) m.mark(path4) print(m) - m.clear(path4) \ No newline at end of file + m.clear(path4) diff --git a/Chapter2/missionaries.py b/Chapter2/missionaries.py index 6bcfd78..d964a1c 100644 --- a/Chapter2/missionaries.py +++ b/Chapter2/missionaries.py @@ -38,7 +38,9 @@ def __str__(self) -> str: def __hash__(self) -> int: return hash((self.wm, self.wc, self.em, self.ec, self.boat)) - def __eq__(self, other: MCState): + def __eq__(self, other: object) -> bool: + if not isinstance(other, MCState): + return NotImplemented return all([getattr(self, el) == getattr(other, el) for el in ["wm", "wc", "em", "ec", "boat"]]) def goal_test(self) -> bool: @@ -46,15 +48,15 @@ def goal_test(self) -> bool: @property def is_legal(self) -> bool: - if self.wm < self.wc and self.wm > 0: + if self.wc > self.wm > 0: return False - if self.em < self.ec and self.em > 0: + if self.ec > self.em > 0: return False return True def successors(self) -> List[MCState]: sucs: List[MCState] = [] - if self.boat: # boat on west bank + if self.boat: # boat on west bank if self.wm > 1: sucs.append(MCState(self.wm - 2, self.wc, not self.boat)) if self.wm > 0: @@ -65,7 +67,7 @@ def successors(self) -> List[MCState]: sucs.append(MCState(self.wm, self.wc - 1, not self.boat)) if (self.wc > 0) and (self.wm > 0): sucs.append(MCState(self.wm - 1, self.wc - 1, not self.boat)) - else: # boat on east bank + else: # boat on east bank if self.em > 1: sucs.append(MCState(self.wm + 2, self.wc, not self.boat)) if self.em > 0: From f798e27ce726cd337fc3f5d5c84a24568d5f6373 Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Fri, 27 Dec 2019 20:02:51 -0500 Subject: [PATCH 12/17] :zap: Ch3 -- cosmetic changes --- Chapter3/csp.py | 28 ++++++++++++++++------------ Chapter3/map_coloring.py | 6 +++--- Chapter3/queens.py | 27 +++++++++++++++------------ Chapter3/send_more_money.py | 10 +++++----- Chapter3/word_search.py | 26 +++++++++++++------------- 5 files changed, 52 insertions(+), 45 deletions(-) diff --git a/Chapter3/csp.py b/Chapter3/csp.py index c8f9ce3..681f9f3 100644 --- a/Chapter3/csp.py +++ b/Chapter3/csp.py @@ -13,11 +13,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Generic, TypeVar, Dict, List, Optional +from typing import Generic, TypeVar, Dict, List, Optional, Iterator from abc import ABC, abstractmethod -V = TypeVar('V') # variable type -D = TypeVar('D') # domain type +V = TypeVar('V') # variable type +D = TypeVar('D') # domain type # Base class for all constraints @@ -37,8 +37,9 @@ def satisfied(self, assignment: Dict[V, D]) -> bool: # that determine whether a particular variable's domain selection is valid class CSP(Generic[V, D]): def __init__(self, variables: List[V], domains: Dict[V, List[D]]) -> None: - self.variables: List[V] = variables # variables to be constrained - self.domains: Dict[V, List[D]] = domains # domain of each variable + self.search_counter = 0 # count of backtracking_search calls + self.variables = variables # variables to be constrained + self.domains = domains # domain of each variable self.constraints: Dict[V, List[Constraint[V, D]]] = {} for variable in self.variables: self.constraints[variable] = [] @@ -60,22 +61,25 @@ def consistent(self, variable: V, assignment: Dict[V, D]) -> bool: return False return True - def backtracking_search(self, assignment: Dict[V, D] = {}) -> Optional[Dict[V, D]]: + def backtracking_search(self, assignment: Dict[V, D] = None) -> Optional[Dict[V, D]]: + self.search_counter += 1 + assignment = {} if assignment is None else assignment.copy() + # assignment is complete if every variable is assigned (our base case) if len(assignment) == len(self.variables): + print("search_counter: {}".format(self.search_counter)) return assignment # get all variables in the CSP but not in the assignment - unassigned: List[V] = [v for v in self.variables if v not in assignment] + unassigned: Iterator[V] = (v for v in self.variables if v not in assignment) # get the every possible domain value of the first unassigned variable - first: V = unassigned[0] + first: V = next(unassigned) for value in self.domains[first]: - local_assignment = assignment.copy() - local_assignment[first] = value + assignment[first] = value # if we're still consistent, we recurse (continue) - if self.consistent(first, local_assignment): - result: Optional[Dict[V, D]] = self.backtracking_search(local_assignment) + if self.consistent(first, assignment): + result: Optional[Dict[V, D]] = self.backtracking_search(assignment) # if we didn't find the result, we will end up backtracking if result is not None: return result diff --git a/Chapter3/map_coloring.py b/Chapter3/map_coloring.py index 4332f11..640f5fa 100644 --- a/Chapter3/map_coloring.py +++ b/Chapter3/map_coloring.py @@ -20,8 +20,8 @@ class MapColoringConstraint(Constraint[str, str]): def __init__(self, place1: str, place2: str) -> None: super().__init__([place1, place2]) - self.place1: str = place1 - self.place2: str = place2 + self.place1 = place1 + self.place2 = place2 def satisfied(self, assignment: Dict[str, str]) -> bool: # If either place is not in the assignment then it is not @@ -54,4 +54,4 @@ def satisfied(self, assignment: Dict[str, str]) -> bool: if solution is None: print("No solution found!") else: - print(solution) \ No newline at end of file + print(solution) diff --git a/Chapter3/queens.py b/Chapter3/queens.py index 31a82a5..95a3b8f 100644 --- a/Chapter3/queens.py +++ b/Chapter3/queens.py @@ -20,7 +20,7 @@ class QueensConstraint(Constraint[int, int]): def __init__(self, columns: List[int]) -> None: super().__init__(columns) - self.columns: List[int] = columns + self.columns = columns def satisfied(self, assignment: Dict[int, int]) -> bool: # q1c = queen 1 column, q1r = queen 1 row @@ -28,23 +28,26 @@ def satisfied(self, assignment: Dict[int, int]) -> bool: # q2c = queen 2 column for q2c in range(q1c + 1, len(self.columns) + 1): if q2c in assignment: - q2r: int = assignment[q2c] # q2r = queen 2 row - if q1r == q2r: # same row? + q2r = assignment[q2c] # q2r = queen 2 row + if q1r == q2r: # same row? return False - if abs(q1r - q2r) == abs(q1c - q2c): # same diagonal? + if abs(q1r - q2r) == abs(q1c - q2c): # same diagonal? return False - return True # no conflict + return True # no conflict +COLUMNS = [1, 2, 3, 4, 5, 6, 7, 8] +ROWS = COLUMNS + if __name__ == "__main__": - columns: List[int] = [1, 2, 3, 4, 5, 6, 7, 8] - rows: Dict[int, List[int]] = {} - for column in columns: - rows[column] = [1, 2, 3, 4, 5, 6, 7, 8] - csp: CSP[int, int] = CSP(columns, rows) - csp.add_constraint(QueensConstraint(columns)) + variables: List[int] = COLUMNS + domains: Dict[int, List[int]] = {} + for column in variables: + domains[column] = ROWS + csp: CSP[int, int] = CSP(variables, domains) + csp.add_constraint(QueensConstraint(variables)) solution: Optional[Dict[int, int]] = csp.backtracking_search() if solution is None: print("No solution found!") else: - print(solution) \ No newline at end of file + print(solution) diff --git a/Chapter3/send_more_money.py b/Chapter3/send_more_money.py index 2071c10..538c51e 100644 --- a/Chapter3/send_more_money.py +++ b/Chapter3/send_more_money.py @@ -41,17 +41,17 @@ def satisfied(self, assignment: Dict[str, int]) -> bool: more: int = m * 1000 + o * 100 + r * 10 + e money: int = m * 10000 + o * 1000 + n * 100 + e * 10 + y return send + more == money - return True # no conflict + return True # no conflict if __name__ == "__main__": - letters: List[str] = ["S", "E", "N", "D", "M", "O", "R", "Y"] + all_letters: List[str] = ["S", "E", "N", "D", "M", "O", "R", "Y"] possible_digits: Dict[str, List[int]] = {} - for letter in letters: + for letter in all_letters: possible_digits[letter] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] possible_digits["M"] = [1] # so we don't get answers starting with a 0 - csp: CSP[str, int] = CSP(letters, possible_digits) - csp.add_constraint(SendMoreMoneyConstraint(letters)) + csp: CSP[str, int] = CSP(all_letters, possible_digits) + csp.add_constraint(SendMoreMoneyConstraint(all_letters)) solution: Optional[Dict[str, int]] = csp.backtracking_search() if solution is None: print("No solution found!") diff --git a/Chapter3/word_search.py b/Chapter3/word_search.py index c6a6085..3e55ad6 100644 --- a/Chapter3/word_search.py +++ b/Chapter3/word_search.py @@ -28,7 +28,7 @@ class GridLocation(NamedTuple): def generate_grid(rows: int, columns: int) -> Grid: # initialize grid with random letters - return [[choice(ascii_uppercase) for c in range(columns)] for r in range(rows)] + return [[choice(ascii_uppercase) for _c in range(columns)] for _r in range(rows)] def display_grid(grid: Grid) -> None: @@ -67,27 +67,27 @@ def __init__(self, words: List[str]) -> None: def satisfied(self, assignment: Dict[str, List[GridLocation]]) -> bool: # if there are any duplicates grid locations then there is an overlap - all_locations = [locs for values in assignment.values() for locs in values] + all_locations = [locs for lists in assignment.values() for locs in lists] return len(set(all_locations)) == len(all_locations) if __name__ == "__main__": - grid: Grid = generate_grid(9, 9) - words: List[str] = ["MATTHEW", "JOE", "MARY", "SARAH", "SALLY"] + the_grid: Grid = generate_grid(9, 9) + the_words: List[str] = ["MATTHEW", "JOE", "MARY", "SARAH", "SALLY"] locations: Dict[str, List[List[GridLocation]]] = {} - for word in words: - locations[word] = generate_domain(word, grid) - csp: CSP[str, List[GridLocation]] = CSP(words, locations) - csp.add_constraint(WordSearchConstraint(words)) + for wrd in the_words: + locations[wrd] = generate_domain(wrd, the_grid) + csp: CSP[str, List[GridLocation]] = CSP(the_words, locations) + csp.add_constraint(WordSearchConstraint(the_words)) solution: Optional[Dict[str, List[GridLocation]]] = csp.backtracking_search() if solution is None: print("No solution found!") else: - for word, grid_locations in solution.items(): + for wrd, grid_locations in solution.items(): # random reverse half the time if choice([True, False]): grid_locations.reverse() - for index, letter in enumerate(word): - (row, col) = (grid_locations[index].row, grid_locations[index].column) - grid[row][col] = letter - display_grid(grid) + for index, letter in enumerate(wrd): + (_row, _col) = (grid_locations[index].row, grid_locations[index].column) + the_grid[_row][_col] = letter + display_grid(the_grid) From 80f2b598941638f09cd956cfa4f8f6588fcb8d53 Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Tue, 31 Dec 2019 12:26:05 -0500 Subject: [PATCH 13/17] :sparkles: Ch04 -- cosmetic changes --- Chapter2/__init__.py | 0 Chapter4/dijkstra.py | 39 ++++++++++++++++++++------------ Chapter4/edge.py | 4 ++-- Chapter4/generic_search.py | 1 + Chapter4/graph.py | 46 +++++++++++++++++++------------------- Chapter4/mst.py | 34 +++++++++++++++------------- Chapter4/priority_queue.py | 2 +- Chapter4/weighted_graph.py | 20 +++++++++-------- __init__.py | 0 9 files changed, 81 insertions(+), 65 deletions(-) create mode 100644 Chapter2/__init__.py create mode 120000 Chapter4/generic_search.py create mode 100644 __init__.py diff --git a/Chapter2/__init__.py b/Chapter2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Chapter4/dijkstra.py b/Chapter4/dijkstra.py index 9bdee27..7c0a752 100644 --- a/Chapter4/dijkstra.py +++ b/Chapter4/dijkstra.py @@ -14,14 +14,14 @@ # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations -from typing import TypeVar, List, Optional, Tuple, Dict +from typing import TypeVar, List, Optional, Tuple, Dict, cast from dataclasses import dataclass from mst import WeightedPath, print_weighted_path from weighted_graph import WeightedGraph from weighted_edge import WeightedEdge from priority_queue import PriorityQueue -V = TypeVar('V') # type of the vertices in the graph +V = TypeVar('V') # type of the vertices in the graph @dataclass @@ -32,26 +32,34 @@ class DijkstraNode: def __lt__(self, other: DijkstraNode) -> bool: return self.distance < other.distance - def __eq__(self, other: DijkstraNode) -> bool: + def __eq__(self, other: object) -> bool: + if not isinstance(other, DijkstraNode): + return NotImplemented return self.distance == other.distance + def vertex_distance(self) -> Tuple[int, float]: + return self.vertex, self.distance + def dijkstra(wg: WeightedGraph[V], root: V) -> Tuple[List[Optional[float]], Dict[int, WeightedEdge]]: - first: int = wg.index_of(root) # find starting index + first: int = wg.index_of(root) # find starting index # distances are unknown at first distances: List[Optional[float]] = [None] * wg.vertex_count - distances[first] = 0 # the root is 0 away from the root - path_dict: Dict[int, WeightedEdge] = {} # how we got to each vertex + distances[first] = 0 # the root is 0 away from the root + path_dict: Dict[int, WeightedEdge] = {} # how we got to each vertex pq: PriorityQueue[DijkstraNode] = PriorityQueue() pq.push(DijkstraNode(first, 0)) while not pq.empty: - u: int = pq.pop().vertex # explore the next closest vertex - dist_u: float = distances[u] # should already have seen it + u, d = pq.pop().vertex_distance() # explore the next closest vertex + dist_u: float = cast(float, distances[u]) # should already have seen it + if d > dist_u: + print("Vadim's Inefficiancy Fix") + continue # look at every edge/vertex from the vertex in question for we in wg.edges_for_index(u): # the old distance to this vertex - dist_v: float = distances[we.v] + dist_v: float = cast(float, distances[we.v]) # no old distance or found shorter path if dist_v is None or dist_v > we.weight + dist_u: # update distance to this vertex @@ -87,7 +95,9 @@ def path_dict_to_path(start: int, end: int, path_dict: Dict[int, WeightedEdge]) if __name__ == "__main__": - city_graph2: WeightedGraph[str] = WeightedGraph(["Seattle", "San Francisco", "Los Angeles", "Riverside", "Phoenix", "Chicago", "Boston", "New York", "Atlanta", "Miami", "Dallas", "Houston", "Detroit", "Philadelphia", "Washington"]) + city_graph2: WeightedGraph[str] = WeightedGraph( + ["Seattle", "San Francisco", "Los Angeles", "Riverside", "Phoenix", "Chicago", "Boston", "New York", "Atlanta", + "Miami", "Dallas", "Houston", "Detroit", "Philadelphia", "Washington"]) city_graph2.add_edge_by_vertices("Seattle", "Chicago", 1737) city_graph2.add_edge_by_vertices("Seattle", "San Francisco", 678) @@ -116,13 +126,14 @@ def path_dict_to_path(start: int, end: int, path_dict: Dict[int, WeightedEdge]) city_graph2.add_edge_by_vertices("New York", "Philadelphia", 81) city_graph2.add_edge_by_vertices("Philadelphia", "Washington", 123) - distances, path_dict = dijkstra(city_graph2, "Los Angeles") - name_distance: Dict[str, Optional[int]] = distance_array_to_vertex_dict(city_graph2, distances) + all_distances, all_path_dict = dijkstra(city_graph2, "Los Angeles") + name_distance: Dict[str, Optional[float]] = distance_array_to_vertex_dict(city_graph2, all_distances) print("Distances from Los Angeles:") for key, value in name_distance.items(): print(f"{key} : {value}") - print("") # blank line + print("") # blank line print("Shortest path from Los Angeles to Boston:") - path: WeightedPath = path_dict_to_path(city_graph2.index_of("Los Angeles"), city_graph2.index_of("Boston"), path_dict) + path: WeightedPath = path_dict_to_path(city_graph2.index_of("Los Angeles"), city_graph2.index_of("Boston"), + all_path_dict) print_weighted_path(city_graph2, path) diff --git a/Chapter4/edge.py b/Chapter4/edge.py index d3887d8..2d7c479 100644 --- a/Chapter4/edge.py +++ b/Chapter4/edge.py @@ -19,8 +19,8 @@ @dataclass class Edge: - u: int # the "from" vertex - v: int # the "to" vertex + u: int # the "from" vertex + v: int # the "to" vertex def reversed(self) -> Edge: return Edge(self.v, self.u) diff --git a/Chapter4/generic_search.py b/Chapter4/generic_search.py new file mode 120000 index 0000000..70a5009 --- /dev/null +++ b/Chapter4/generic_search.py @@ -0,0 +1 @@ +../Chapter2/generic_search.py \ No newline at end of file diff --git a/Chapter4/graph.py b/Chapter4/graph.py index f08601e..6f95d96 100644 --- a/Chapter4/graph.py +++ b/Chapter4/graph.py @@ -13,41 +13,42 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypeVar, Generic, List, Optional +from typing import TypeVar, Generic, List, cast from edge import Edge +V = TypeVar('V') # type of the vertices in the graph +E = TypeVar('E', bound=Edge) -V = TypeVar('V') # type of the vertices in the graph - -class Graph(Generic[V]): - def __init__(self, vertices: List[V] = []) -> None: +class Graph(Generic[V, E]): + def __init__(self, vertices: List[V] = None) -> None: + vertices = [] if vertices is None else vertices self._vertices: List[V] = vertices - self._edges: List[List[Edge]] = [[] for _ in vertices] + self._edges: List[List[E]] = [[] for _ in vertices] @property def vertex_count(self) -> int: - return len(self._vertices) # Number of vertices + return len(self._vertices) # Number of vertices @property def edge_count(self) -> int: - return sum(map(len, self._edges)) # Number of edges + return sum(map(len, self._edges)) # Number of edges # Add a vertex to the graph and return its index def add_vertex(self, vertex: V) -> int: self._vertices.append(vertex) - self._edges.append([]) # add empty list for containing edges - return self.vertex_count - 1 # return index of added vertex + self._edges.append([]) # add empty list for containing edges + return self.vertex_count - 1 # return index of added vertex # This is an undirected graph, # so we always add edges in both directions - def add_edge(self, edge: Edge) -> None: + def add_edge(self, edge: E) -> None: self._edges[edge.u].append(edge) - self._edges[edge.v].append(edge.reversed()) + self._edges[edge.v].append(cast(E, edge.reversed())) # Add an edge using vertex indices (convenience method) def add_edge_by_indices(self, u: int, v: int) -> None: - edge: Edge = Edge(u, v) + edge: E = cast(E, Edge(u, v)) self.add_edge(edge) # Add an edge by looking up vertex indices (convenience method) @@ -68,16 +69,16 @@ def index_of(self, vertex: V) -> int: def neighbors_for_index(self, index: int) -> List[V]: return list(map(self.vertex_at, [e.v for e in self._edges[index]])) - # Lookup a vertice's index and find its neighbors (convenience method) + # Lookup a vertex index and find its neighbors (convenience method) def neighbors_for_vertex(self, vertex: V) -> List[V]: return self.neighbors_for_index(self.index_of(vertex)) # Return all of the edges associated with a vertex at some index - def edges_for_index(self, index: int) -> List[Edge]: + def edges_for_index(self, index: int) -> List[E]: return self._edges[index] # Lookup the index of a vertex and return its edges (convenience method) - def edges_for_vertex(self, vertex: V) -> List[Edge]: + def edges_for_vertex(self, vertex: V) -> List[E]: return self.edges_for_index(self.index_of(vertex)) # Make it easy to pretty-print a Graph @@ -90,7 +91,9 @@ def __str__(self) -> str: if __name__ == "__main__": # test basic Graph construction - city_graph: Graph[str] = Graph(["Seattle", "San Francisco", "Los Angeles", "Riverside", "Phoenix", "Chicago", "Boston", "New York", "Atlanta", "Miami", "Dallas", "Houston", "Detroit", "Philadelphia", "Washington"]) + city_graph: Graph[str, Edge] = Graph( + ["Seattle", "San Francisco", "Los Angeles", "Riverside", "Phoenix", "Chicago", "Boston", "New York", "Atlanta", + "Miami", "Dallas", "Houston", "Detroit", "Philadelphia", "Washington"]) city_graph.add_edge_by_vertices("Seattle", "Chicago") city_graph.add_edge_by_vertices("Seattle", "San Francisco") city_graph.add_edge_by_vertices("San Francisco", "Riverside") @@ -120,15 +123,12 @@ def __str__(self) -> str: print(city_graph) # Reuse BFS from Chapter 2 on city_graph - import sys - sys.path.insert(0, '..') # so we can access the Chapter2 package in the parent directory - from Chapter2.generic_search import bfs, Node, node_to_path + from generic_search import bfs, node_to_path - bfs_result: Optional[Node[V]] = bfs("Boston", lambda x: x == "Miami", city_graph.neighbors_for_vertex) + bfs_result = bfs("Boston", lambda x: x == "Miami", city_graph.neighbors_for_vertex) if bfs_result is None: print("No solution found using breadth-first search!") else: - path: List[V] = node_to_path(bfs_result) + path = node_to_path(bfs_result) print("Path from Boston to Miami:") print(path) - diff --git a/Chapter4/mst.py b/Chapter4/mst.py index 0fbaeaf..d5b84b1 100644 --- a/Chapter4/mst.py +++ b/Chapter4/mst.py @@ -18,8 +18,8 @@ from weighted_edge import WeightedEdge from priority_queue import PriorityQueue -V = TypeVar('V') # type of the vertices in the graph -WeightedPath = List[WeightedEdge] # type alias for paths +V = TypeVar('V') # type of the vertices in the graph +WeightedPath = List[WeightedEdge] # type alias for paths def total_weight(wp: WeightedPath) -> float: @@ -29,26 +29,26 @@ def total_weight(wp: WeightedPath) -> float: def mst(wg: WeightedGraph[V], start: int = 0) -> Optional[WeightedPath]: if start > (wg.vertex_count - 1) or start < 0: return None - result: WeightedPath = [] # holds the final MST + result: WeightedPath = [] # holds the final MST pq: PriorityQueue[WeightedEdge] = PriorityQueue() - visited: [bool] = [False] * wg.vertex_count # where we've been + visited: List[bool] = [False] * wg.vertex_count # where we've been def visit(index: int): - visited[index] = True # mark as visited - for edge in wg.edges_for_index(index): + visited[index] = True # mark as visited + for _edge in wg.edges_for_index(index): # add all edges coming from here to pq - if not visited[edge.v]: - pq.push(edge) + if not visited[_edge.v]: + pq.push(_edge) - visit(start) # the first vertex is where everything begins + visit(start) # the first vertex is where everything begins - while not pq.empty: # keep going while there are edges to process + while not pq.empty: # keep going while there are edges to process edge = pq.pop() if visited[edge.v]: - continue # don't ever revisit + continue # don't ever revisit # this is the current smallest, so add it to solution result.append(edge) - visit(edge.v) # visit where this connects + visit(edge.v) # visit where this connects return result @@ -60,7 +60,9 @@ def print_weighted_path(wg: WeightedGraph, wp: WeightedPath) -> None: if __name__ == "__main__": - city_graph2: WeightedGraph[str] = WeightedGraph(["Seattle", "San Francisco", "Los Angeles", "Riverside", "Phoenix", "Chicago", "Boston", "New York", "Atlanta", "Miami", "Dallas", "Houston", "Detroit", "Philadelphia", "Washington"]) + city_graph2: WeightedGraph[str] = WeightedGraph( + ["Seattle", "San Francisco", "Los Angeles", "Riverside", "Phoenix", "Chicago", "Boston", "New York", "Atlanta", + "Miami", "Dallas", "Houston", "Detroit", "Philadelphia", "Washington"]) city_graph2.add_edge_by_vertices("Seattle", "Chicago", 1737) city_graph2.add_edge_by_vertices("Seattle", "San Francisco", 678) @@ -89,8 +91,8 @@ def print_weighted_path(wg: WeightedGraph, wp: WeightedPath) -> None: city_graph2.add_edge_by_vertices("New York", "Philadelphia", 81) city_graph2.add_edge_by_vertices("Philadelphia", "Washington", 123) - result: Optional[WeightedPath] = mst(city_graph2) - if result is None: + mst_result: Optional[WeightedPath] = mst(city_graph2) + if mst_result is None: print("No solution found!") else: - print_weighted_path(city_graph2, result) \ No newline at end of file + print_weighted_path(city_graph2, mst_result) diff --git a/Chapter4/priority_queue.py b/Chapter4/priority_queue.py index a8dfc81..81db9c3 100644 --- a/Chapter4/priority_queue.py +++ b/Chapter4/priority_queue.py @@ -35,4 +35,4 @@ def pop(self) -> T: return heappop(self._container) # out by priority def __repr__(self) -> str: - return repr(self._container) \ No newline at end of file + return repr(self._container) diff --git a/Chapter4/weighted_graph.py b/Chapter4/weighted_graph.py index 0acb273..fa25ff2 100644 --- a/Chapter4/weighted_graph.py +++ b/Chapter4/weighted_graph.py @@ -17,19 +17,19 @@ from graph import Graph from weighted_edge import WeightedEdge -V = TypeVar('V') # type of the vertices in the graph +V = TypeVar('V') # type of the vertices in the graph -class WeightedGraph(Generic[V], Graph[V]): - def __init__(self, vertices: List[V] = []) -> None: - self._vertices: List[V] = vertices - self._edges: List[List[WeightedEdge]] = [[] for _ in vertices] +class WeightedGraph(Generic[V], Graph[V, WeightedEdge]): + def __init__(self, vertices: List[V] = None) -> None: + super().__init__(vertices) + self._edges: List[List[WeightedEdge]] = [[] for _ in self._vertices] - def add_edge_by_indices(self, u: int, v: int, weight: float) -> None: + def add_edge_by_indices(self, u: int, v: int, weight: float = 0) -> None: edge: WeightedEdge = WeightedEdge(u, v, weight) - self.add_edge(edge) # call superclass version + self.add_edge(edge) # call superclass version - def add_edge_by_vertices(self, first: V, second: V, weight: float) -> None: + def add_edge_by_vertices(self, first: V, second: V, weight: float = 0) -> None: u: int = self._vertices.index(first) v: int = self._vertices.index(second) self.add_edge_by_indices(u, v, weight) @@ -48,7 +48,9 @@ def __str__(self) -> str: if __name__ == "__main__": - city_graph2: WeightedGraph[str] = WeightedGraph(["Seattle", "San Francisco", "Los Angeles", "Riverside", "Phoenix", "Chicago", "Boston", "New York", "Atlanta", "Miami", "Dallas", "Houston", "Detroit", "Philadelphia", "Washington"]) + city_graph2: WeightedGraph[str] = WeightedGraph( + ["Seattle", "San Francisco", "Los Angeles", "Riverside", "Phoenix", "Chicago", "Boston", "New York", "Atlanta", + "Miami", "Dallas", "Houston", "Detroit", "Philadelphia", "Washington"]) city_graph2.add_edge_by_vertices("Seattle", "Chicago", 1737) city_graph2.add_edge_by_vertices("Seattle", "San Francisco", 678) diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 From 49a78918ab5eaf87cba9700bb1bc6d5c6b4d80d0 Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Fri, 3 Jan 2020 11:06:17 -0500 Subject: [PATCH 14/17] :apple: Ch05 -- cosmetic changes --- Chapter5/chromosome.py | 2 +- Chapter5/genetic_algorithm.py | 23 +++++++++++++---------- Chapter5/list_compression.py | 10 +++++++--- Chapter5/send_more_money2.py | 7 +++++-- Chapter5/simple_equation.py | 10 ++++++---- 5 files changed, 32 insertions(+), 20 deletions(-) diff --git a/Chapter5/chromosome.py b/Chapter5/chromosome.py index 3b26a40..b742a9d 100644 --- a/Chapter5/chromosome.py +++ b/Chapter5/chromosome.py @@ -17,7 +17,7 @@ from typing import TypeVar, Tuple, Type from abc import ABC, abstractmethod -T = TypeVar('T', bound='Chromosome') # for returning self +T = TypeVar('T', bound='Chromosome') # for returning self # Base class for all chromosomes; all methods must be overridden diff --git a/Chapter5/genetic_algorithm.py b/Chapter5/genetic_algorithm.py index 7304882..ba9d8b5 100644 --- a/Chapter5/genetic_algorithm.py +++ b/Chapter5/genetic_algorithm.py @@ -14,20 +14,24 @@ # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations -from typing import TypeVar, Generic, List, Tuple, Callable +from typing import TypeVar, Generic, List, Tuple, Callable, cast from enum import Enum from random import choices, random from heapq import nlargest from statistics import mean from chromosome import Chromosome -C = TypeVar('C', bound=Chromosome) # type of the chromosomes +C = TypeVar('C', bound=Chromosome) # type of the chromosomes class GeneticAlgorithm(Generic[C]): - SelectionType = Enum("SelectionType", "ROULETTE TOURNAMENT") + class SelectionType(Enum): + ROULETTE = 1 + TOURNAMENT = 2 - def __init__(self, initial_population: List[C], threshold: float, max_generations: int = 100, mutation_chance: float = 0.01, crossover_chance: float = 0.7, selection_type: SelectionType = SelectionType.TOURNAMENT) -> None: + def __init__(self, initial_population: List[C], threshold: float, max_generations: int = 100, + mutation_chance: float = 0.01, crossover_chance: float = 0.7, + selection_type: SelectionType = SelectionType.TOURNAMENT) -> None: self._population: List[C] = initial_population self._threshold: float = threshold self._max_generations: int = max_generations @@ -39,12 +43,12 @@ def __init__(self, initial_population: List[C], threshold: float, max_generation # Use the probability distribution wheel to pick 2 parents # Note: will not work with negative fitness results def _pick_roulette(self, wheel: List[float]) -> Tuple[C, C]: - return tuple(choices(self._population, weights=wheel, k=2)) + return cast(Tuple[C, C], tuple(choices(self._population, weights=wheel, k=2))) # Choose num_participants at random and take the best 2 def _pick_tournament(self, num_participants: int) -> Tuple[C, C]: participants: List[C] = choices(self._population, k=num_participants) - return tuple(nlargest(2, participants, key=self._fitness_key)) + return cast(Tuple[C, C], tuple(nlargest(2, participants, key=self._fitness_key))) # Replace the population with a new generation of individuals def _reproduce_and_replace(self) -> None: @@ -64,7 +68,7 @@ def _reproduce_and_replace(self) -> None: # if we had an odd number, we'll have 1 extra, so we remove it if len(new_population) > len(self._population): new_population.pop() - self._population = new_population # replace reference + self._population = new_population # replace reference # With _mutation_chance probability mutate each individual def _mutate(self) -> None: @@ -85,6 +89,5 @@ def run(self) -> C: self._mutate() highest: C = max(self._population, key=self._fitness_key) if highest.fitness() > best.fitness(): - best = highest # found a new best - return best # best we found in _max_generations - + best = highest # found a new best + return best # best we found in _max_generations diff --git a/Chapter5/list_compression.py b/Chapter5/list_compression.py index c85e32d..19d4dc2 100644 --- a/Chapter5/list_compression.py +++ b/Chapter5/list_compression.py @@ -24,7 +24,8 @@ from pickle import dumps # 165 bytes compressed -PEOPLE: List[str] = ["Michael", "Sarah", "Joshua", "Narine", "David", "Sajid", "Melanie", "Daniel", "Wei", "Dean", "Brian", "Murat", "Lisa"] +PEOPLE: List[str] = ["Michael", "Sarah", "Joshua", "Narine", "David", "Sajid", "Melanie", "Daniel", "Wei", "Dean", + "Brian", "Murat", "Lisa"] class ListCompression(Chromosome): @@ -53,7 +54,7 @@ def crossover(self, other: ListCompression) -> Tuple[ListCompression, ListCompre child2.lst[child2.lst.index(l1)], child2.lst[idx1] = child2.lst[idx1], l1 return child1, child2 - def mutate(self) -> None: # swap two locations + def mutate(self) -> None: # swap two locations idx1, idx2 = sample(range(len(self.lst)), k=2) self.lst[idx1], self.lst[idx2] = self.lst[idx2], self.lst[idx1] @@ -63,7 +64,10 @@ def __str__(self) -> str: if __name__ == "__main__": initial_population: List[ListCompression] = [ListCompression.random_instance() for _ in range(100)] - ga: GeneticAlgorithm[ListCompression] = GeneticAlgorithm(initial_population=initial_population, threshold=1.0, max_generations = 100, mutation_chance = 0.2, crossover_chance = 0.7, selection_type=GeneticAlgorithm.SelectionType.TOURNAMENT) + ga: GeneticAlgorithm[ListCompression] = GeneticAlgorithm(initial_population=initial_population, threshold=1.0, + max_generations=100, mutation_chance=0.1, + crossover_chance=0.7, + selection_type=GeneticAlgorithm.SelectionType.TOURNAMENT) result: ListCompression = ga.run() print(result) diff --git a/Chapter5/send_more_money2.py b/Chapter5/send_more_money2.py index 9b43b48..ba4abd0 100644 --- a/Chapter5/send_more_money2.py +++ b/Chapter5/send_more_money2.py @@ -55,7 +55,7 @@ def crossover(self, other: SendMoreMoney2) -> Tuple[SendMoreMoney2, SendMoreMone child2.letters[child2.letters.index(l1)], child2.letters[idx1] = child2.letters[idx1], l1 return child1, child2 - def mutate(self) -> None: # swap two letters' locations + def mutate(self) -> None: # swap two letters' locations idx1, idx2 = sample(range(len(self.letters)), k=2) self.letters[idx1], self.letters[idx2] = self.letters[idx2], self.letters[idx1] @@ -77,6 +77,9 @@ def __str__(self) -> str: if __name__ == "__main__": initial_population: List[SendMoreMoney2] = [SendMoreMoney2.random_instance() for _ in range(1000)] - ga: GeneticAlgorithm[SendMoreMoney2] = GeneticAlgorithm(initial_population=initial_population, threshold=1.0, max_generations = 1000, mutation_chance = 0.2, crossover_chance = 0.7, selection_type=GeneticAlgorithm.SelectionType.ROULETTE) + ga: GeneticAlgorithm[SendMoreMoney2] = GeneticAlgorithm(initial_population=initial_population, threshold=1.0, + max_generations=1000, mutation_chance=0.2, + crossover_chance=0.7, + selection_type=GeneticAlgorithm.SelectionType.ROULETTE) result: SendMoreMoney2 = ga.run() print(result) diff --git a/Chapter5/simple_equation.py b/Chapter5/simple_equation.py index d18f413..1ea5342 100644 --- a/Chapter5/simple_equation.py +++ b/Chapter5/simple_equation.py @@ -26,7 +26,7 @@ def __init__(self, x: int, y: int) -> None: self.x: int = x self.y: int = y - def fitness(self) -> float: # 6x - x^2 + 4y - y^2 + def fitness(self) -> float: # 6x - x^2 + 4y - y^2 return 6 * self.x - self.x * self.x + 4 * self.y - self.y * self.y @classmethod @@ -41,12 +41,12 @@ def crossover(self, other: SimpleEquation) -> Tuple[SimpleEquation, SimpleEquati return child1, child2 def mutate(self) -> None: - if random() > 0.5: # mutate x + if random() > 0.5: # mutate x if random() > 0.5: self.x += 1 else: self.x -= 1 - else: # otherwise mutate y + else: # otherwise mutate y if random() > 0.5: self.y += 1 else: @@ -58,6 +58,8 @@ def __str__(self) -> str: if __name__ == "__main__": initial_population: List[SimpleEquation] = [SimpleEquation.random_instance() for _ in range(20)] - ga: GeneticAlgorithm[SimpleEquation] = GeneticAlgorithm(initial_population=initial_population, threshold=13.0, max_generations = 100, mutation_chance = 0.1, crossover_chance = 0.7) + ga: GeneticAlgorithm[SimpleEquation] = GeneticAlgorithm(initial_population=initial_population, threshold=13.0, + max_generations=100, mutation_chance=0.1, + crossover_chance=0.7) result: SimpleEquation = ga.run() print(result) From 2d852e3d04f23445fc3e70a7b81ced64b52916e6 Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Sun, 5 Jan 2020 22:49:36 -0500 Subject: [PATCH 15/17] :goal_net: Ch06 -- cosmetic changes --- Chapter6/kmeans.py | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/Chapter6/kmeans.py b/Chapter6/kmeans.py index fdff5cf..892ee75 100644 --- a/Chapter6/kmeans.py +++ b/Chapter6/kmeans.py @@ -26,30 +26,32 @@ def zscores(original: Sequence[float]) -> List[float]: avg: float = mean(original) std: float = pstdev(original) - if std == 0: # return all zeros if there is no variation + if std == 0: # return all zeros if there is no variation return [0] * len(original) return [(x - avg) / std for x in original] -Point = TypeVar('Point', bound=DataPoint) +Point = TypeVar('Point', bound='DataPoint') + + +@dataclass +class Cluster(Generic[Point]): + points: List[Point] + centroid: DataPoint class KMeans(Generic[Point]): - @dataclass - class Cluster: - points: List[Point] - centroid: DataPoint def __init__(self, k: int, points: List[Point]) -> None: - if k < 1: # k-means can't do negative or zero clusters + if k < 1: # k-means can't do negative or zero clusters raise ValueError("k must be >= 1") self._points: List[Point] = points self._zscore_normalize() # initialize empty clusters with random centroids - self._clusters: List[KMeans.Cluster] = [] + self._clusters: List[Cluster[Point]] = [] for _ in range(k): rand_point: DataPoint = self._random_point() - cluster: KMeans.Cluster = KMeans.Cluster([], rand_point) + cluster: Cluster = Cluster([], rand_point) self._clusters.append(cluster) @property @@ -81,13 +83,13 @@ def _assign_clusters(self) -> None: for point in self._points: closest: DataPoint = min(self._centroids, key=partial(DataPoint.distance, point)) idx: int = self._centroids.index(closest) - cluster: KMeans.Cluster = self._clusters[idx] + cluster: Cluster = self._clusters[idx] cluster.points.append(point) # Find the center of each cluster and move the centroid to there def _generate_centroids(self) -> None: for cluster in self._clusters: - if len(cluster.points) == 0: # keep the same centroid if no points + if len(cluster.points) == 0: # keep the same centroid if no points continue means: List[float] = [] for dimension in range(cluster.points[0].num_dimensions): @@ -95,14 +97,14 @@ def _generate_centroids(self) -> None: means.append(mean(dimension_slice)) cluster.centroid = DataPoint(means) - def run(self, max_iterations: int = 100) -> List[KMeans.Cluster]: + def run(self, max_iterations: int = 100) -> List[Cluster]: for iteration in range(max_iterations): - for cluster in self._clusters: # clear all clusters + for cluster in self._clusters: # clear all clusters cluster.points.clear() - self._assign_clusters() # find cluster each point is closest to - old_centroids: List[DataPoint] = deepcopy(self._centroids) # record - self._generate_centroids() # find new centroids - if old_centroids == self._centroids: # have centroids moved? + self._assign_clusters() # find cluster each point is closest to + old_centroids: List[DataPoint] = deepcopy(self._centroids) # record + self._generate_centroids() # find new centroids + if old_centroids == self._centroids: # have centroids moved? print(f"Converged after {iteration} iterations") return self._clusters return self._clusters @@ -112,7 +114,7 @@ def run(self, max_iterations: int = 100) -> List[KMeans.Cluster]: point1: DataPoint = DataPoint([2.0, 1.0, 1.0]) point2: DataPoint = DataPoint([2.0, 2.0, 5.0]) point3: DataPoint = DataPoint([3.0, 1.5, 2.5]) - kmeans_test: KMeans[DataPoint] = KMeans(2, [point1, point2, point3]) - test_clusters: List[KMeans.Cluster] = kmeans_test.run() + kmeans_test: KMeans[DataPoint] = KMeans(3, [point1, point2, point3]) + test_clusters: List[Cluster] = kmeans_test.run() for index, cluster in enumerate(test_clusters): print(f"Cluster {index}: {cluster.points}") From bc95cb7d692338ba3a8ea6f932cf4b8829115205 Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Wed, 8 Jan 2020 19:22:43 -0500 Subject: [PATCH 16/17] :art: Ch07 -- minor --- Chapter7/layer.py | 8 +++++--- Chapter7/neuron.py | 4 ++-- Chapter7/util.py | 1 - 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Chapter7/layer.py b/Chapter7/layer.py index b133c4b..d3b29de 100644 --- a/Chapter7/layer.py +++ b/Chapter7/layer.py @@ -21,7 +21,9 @@ class Layer: - def __init__(self, previous_layer: Optional[Layer], num_neurons: int, learning_rate: float, activation_function: Callable[[float], float], derivative_activation_function: Callable[[float], float]) -> None: + def __init__(self, previous_layer: Optional[Layer], num_neurons: int, learning_rate: float, + activation_function: Callable[[float], float], + derivative_activation_function: Callable[[float], float]) -> None: self.previous_layer: Optional[Layer] = previous_layer self.neurons: List[Neuron] = [] # the following could all be one large list comprehension, but gets a bit long that way @@ -44,7 +46,8 @@ def outputs(self, inputs: List[float]) -> List[float]: # should only be called on output layer def calculate_deltas_for_output_layer(self, expected: List[float]) -> None: for n in range(len(self.neurons)): - self.neurons[n].delta = self.neurons[n].derivative_activation_function(self.neurons[n].output_cache) * (expected[n] - self.output_cache[n]) + self.neurons[n].delta = self.neurons[n].derivative_activation_function(self.neurons[n].output_cache) * ( + expected[n] - self.output_cache[n]) # should not be called on output layer def calculate_deltas_for_hidden_layer(self, next_layer: Layer) -> None: @@ -53,4 +56,3 @@ def calculate_deltas_for_hidden_layer(self, next_layer: Layer) -> None: next_deltas: List[float] = [n.delta for n in next_layer.neurons] sum_weights_and_deltas: float = dot_product(next_weights, next_deltas) neuron.delta = neuron.derivative_activation_function(neuron.output_cache) * sum_weights_and_deltas - diff --git a/Chapter7/neuron.py b/Chapter7/neuron.py index 4b8e319..ed988e4 100644 --- a/Chapter7/neuron.py +++ b/Chapter7/neuron.py @@ -18,7 +18,8 @@ class Neuron: - def __init__(self, weights: List[float], learning_rate: float, activation_function: Callable[[float], float], derivative_activation_function: Callable[[float], float]) -> None: + def __init__(self, weights: List[float], learning_rate: float, activation_function: Callable[[float], float], + derivative_activation_function: Callable[[float], float]) -> None: self.weights: List[float] = weights self.activation_function: Callable[[float], float] = activation_function self.derivative_activation_function: Callable[[float], float] = derivative_activation_function @@ -29,4 +30,3 @@ def __init__(self, weights: List[float], learning_rate: float, activation_functi def output(self, inputs: List[float]) -> float: self.output_cache = dot_product(inputs, self.weights) return self.activation_function(self.output_cache) - diff --git a/Chapter7/util.py b/Chapter7/util.py index b83a66e..778f379 100644 --- a/Chapter7/util.py +++ b/Chapter7/util.py @@ -41,4 +41,3 @@ def normalize_by_feature_scaling(dataset: List[List[float]]) -> None: minimum = min(column) for row_num in range(len(dataset)): dataset[row_num][col_num] = (dataset[row_num][col_num] - minimum) / (maximum - minimum) - From 25222e9ba836b0e1db66c51ea0581e052043f398 Mon Sep 17 00:00:00 2001 From: Vadim Storozhuk Date: Sat, 11 Jan 2020 12:41:38 -0500 Subject: [PATCH 17/17] :sparkles: Ch08 -- self playing Connect 4 bot --- Chapter8/board.py | 1 - Chapter8/connectfour.py | 11 +++++----- Chapter8/connectfour_autoplay.py | 36 ++++++++++++++++++++++++++++++++ Chapter8/minimax.py | 23 ++++++++++++++------ Chapter8/tictactoe.py | 24 ++++++++++----------- Chapter8/tictactoe_tests.py | 11 ++++++++-- 6 files changed, 79 insertions(+), 27 deletions(-) create mode 100644 Chapter8/connectfour_autoplay.py diff --git a/Chapter8/board.py b/Chapter8/board.py index 5008a58..7a4a18a 100644 --- a/Chapter8/board.py +++ b/Chapter8/board.py @@ -53,4 +53,3 @@ def is_draw(self) -> bool: @abstractmethod def evaluate(self, player: Piece) -> float: ... - diff --git a/Chapter8/connectfour.py b/Chapter8/connectfour.py index 98bfa22..eed1ce1 100644 --- a/Chapter8/connectfour.py +++ b/Chapter8/connectfour.py @@ -22,7 +22,7 @@ class C4Piece(Piece, Enum): B = "B" R = "R" - E = " " # stand-in for empty + E = " " # stand-in for empty @property def opposite(self) -> C4Piece: @@ -38,11 +38,11 @@ def __str__(self) -> str: def generate_segments(num_columns: int, num_rows: int, segment_length: int) -> List[List[Tuple[int, int]]]: - segments: List[List[Tuple[int, int]]] = [] + segments = [] # generate the vertical segments for c in range(num_columns): for r in range(num_rows - segment_length + 1): - segment: List[Tuple[int, int]] = [] + segment = [] for t in range(segment_length): segment.append((c, r + t)) segments.append(segment) @@ -149,11 +149,11 @@ def is_win(self) -> bool: def _evaluate_segment(self, segment: List[Tuple[int, int]], player: Piece) -> float: black_count, red_count = self._count_segment(segment) if red_count > 0 and black_count > 0: - return 0 # mixed segments are neutral + return 0 # mixed segments are neutral count: int = max(red_count, black_count) score: float = 0 if count == 2: - score = 1 + score = 45 elif count == 3: score = 100 elif count == 4: @@ -179,4 +179,3 @@ def __repr__(self) -> str: display += f"{self.position[c][r]}" + "|" display += "\n" return display - diff --git a/Chapter8/connectfour_autoplay.py b/Chapter8/connectfour_autoplay.py new file mode 100644 index 0000000..6fb492b --- /dev/null +++ b/Chapter8/connectfour_autoplay.py @@ -0,0 +1,36 @@ +from minimax import find_best_move +from connectfour import C4Board +from board import Move, Board +from time import sleep +from typing import Tuple + +board: Board = C4Board() + + +def computer_play(board: Board, search_depth: int = 5) -> Tuple[bool, Board]: + end_of_game: bool = False + + computer_move: Move = find_best_move(board, search_depth) + print(f"Computer move is {computer_move}") + board = board.move(computer_move) + print(board) + if board.is_win: + print(f"{board.turn.opposite} wins!") + end_of_game = True + elif board.is_draw: + print("Draw!") + end_of_game = True + return end_of_game, board + + +if __name__ == "__main__": + # main game loop + end_of_game: bool = False + while True: + end_of_game, board = computer_play(board, 4) + if end_of_game: + break + end_of_game, board = computer_play(board, 4) + if end_of_game: + break + # sleep(2) diff --git a/Chapter8/minimax.py b/Chapter8/minimax.py index 0d358ae..d1b8c53 100644 --- a/Chapter8/minimax.py +++ b/Chapter8/minimax.py @@ -17,28 +17,36 @@ from board import Piece, Board, Move +count: int = 0 + + # Find the best possible outcome for original player def minimax(board: Board, maximizing: bool, original_player: Piece, max_depth: int = 8) -> float: + global count + count += 1 # Base case – terminal position or maximum depth reached if board.is_win or board.is_draw or max_depth == 0: return board.evaluate(original_player) # Recursive case - maximize your gains or minimize the opponent's gains if maximizing: - best_eval: float = float("-inf") # arbitrarily low starting point + best_eval: float = float("-inf") # arbitrarily low starting point for move in board.legal_moves: result: float = minimax(board.move(move), False, original_player, max_depth - 1) - best_eval = max(result, best_eval) # we want the move with the highest evaluation + best_eval = max(result, best_eval) # we want the move with the highest evaluation return best_eval - else: # minimizing + else: # minimizing worst_eval: float = float("inf") for move in board.legal_moves: result = minimax(board.move(move), True, original_player, max_depth - 1) - worst_eval = min(result, worst_eval) # we want the move with the lowest evaluation + worst_eval = min(result, worst_eval) # we want the move with the lowest evaluation return worst_eval -def alphabeta(board: Board, maximizing: bool, original_player: Piece, max_depth: int = 8, alpha: float = float("-inf"), beta: float = float("inf")) -> float: +def alphabeta(board: Board, maximizing: bool, original_player: Piece, max_depth: int = 8, alpha: float = float("-inf"), + beta: float = float("inf")) -> float: + global count + count += 1 # Base case – terminal position or maximum depth reached if board.is_win or board.is_draw or max_depth == 0: return board.evaluate(original_player) @@ -70,4 +78,7 @@ def find_best_move(board: Board, max_depth: int = 8) -> Move: if result > best_eval: best_eval = result best_move = move - return best_move \ No newline at end of file + global count + print(f"Nodes visited: {count}") + count = 0 + return best_move diff --git a/Chapter8/tictactoe.py b/Chapter8/tictactoe.py index 74192ff..f62ec9a 100644 --- a/Chapter8/tictactoe.py +++ b/Chapter8/tictactoe.py @@ -22,7 +22,7 @@ class TTTPiece(Piece, Enum): X = "X" O = "O" - E = " " # stand-in for empty + E = " " # stand-in for empty @property def opposite(self) -> TTTPiece: @@ -38,8 +38,8 @@ def __str__(self) -> str: class TTTBoard(Board): - def __init__(self, position: List[TTTPiece] = [TTTPiece.E] * 9, turn: TTTPiece = TTTPiece.X) -> None: - self.position: List[TTTPiece] = position + def __init__(self, position: List[TTTPiece] = None, turn: TTTPiece = TTTPiece.X) -> None: + self.position = [TTTPiece.E] * 9 if position is None else position self._turn: TTTPiece = turn @property @@ -53,19 +53,19 @@ def move(self, location: Move) -> Board: @property def legal_moves(self) -> List[Move]: - return [Move(l) for l in range(len(self.position)) if self.position[l] == TTTPiece.E] + return [Move(loc) for loc in range(len(self.position)) if self.position[loc] == TTTPiece.E] @property def is_win(self) -> bool: # three row, three column, and then two diagonal checks - return self.position[0] == self.position[1] and self.position[0] == self.position[2] and self.position[0] != TTTPiece.E or \ - self.position[3] == self.position[4] and self.position[3] == self.position[5] and self.position[3] != TTTPiece.E or \ - self.position[6] == self.position[7] and self.position[6] == self.position[8] and self.position[6] != TTTPiece.E or \ - self.position[0] == self.position[3] and self.position[0] == self.position[6] and self.position[0] != TTTPiece.E or \ - self.position[1] == self.position[4] and self.position[1] == self.position[7] and self.position[1] != TTTPiece.E or \ - self.position[2] == self.position[5] and self.position[2] == self.position[8] and self.position[2] != TTTPiece.E or \ - self.position[0] == self.position[4] and self.position[0] == self.position[8] and self.position[0] != TTTPiece.E or \ - self.position[2] == self.position[4] and self.position[2] == self.position[6] and self.position[2] != TTTPiece.E + return self.position[0] == self.position[1] == self.position[2] and self.position[0] != TTTPiece.E or \ + self.position[3] == self.position[4] == self.position[5] and self.position[3] != TTTPiece.E or \ + self.position[6] == self.position[7] == self.position[8] and self.position[6] != TTTPiece.E or \ + self.position[0] == self.position[3] == self.position[6] and self.position[0] != TTTPiece.E or \ + self.position[1] == self.position[4] == self.position[7] and self.position[1] != TTTPiece.E or \ + self.position[2] == self.position[5] == self.position[8] and self.position[2] != TTTPiece.E or \ + self.position[0] == self.position[4] == self.position[8] and self.position[0] != TTTPiece.E or \ + self.position[2] == self.position[4] == self.position[6] and self.position[2] != TTTPiece.E def evaluate(self, player: Piece) -> float: if self.is_win and self.turn == player: diff --git a/Chapter8/tictactoe_tests.py b/Chapter8/tictactoe_tests.py index 9864e1a..2ba636a 100644 --- a/Chapter8/tictactoe_tests.py +++ b/Chapter8/tictactoe_tests.py @@ -21,6 +21,15 @@ class TTTMinimaxTestCase(unittest.TestCase): + def test_starting_position(self): + starting_position: List[TTTPiece] = [TTTPiece.E, TTTPiece.E, TTTPiece.E, + TTTPiece.E, TTTPiece.E, TTTPiece.E, + TTTPiece.E, TTTPiece.E, TTTPiece.E] + test_board1: TTTBoard = TTTBoard(starting_position, TTTPiece.X) + answer1: Move = find_best_move(test_board1) + print(f"Best 1st move: {answer1}") + self.assertTrue(True) + def test_easy_position(self): # win in 1 move to_win_easy_position: List[TTTPiece] = [TTTPiece.X, TTTPiece.O, TTTPiece.X, @@ -51,5 +60,3 @@ def test_hard_position(self): if __name__ == '__main__': unittest.main() - -