Files

411 lines
14 KiB
Python

import argparse
import csv
from collections import deque
from copy import copy
from queue import PriorityQueue
from time import time
import sys
class BankState(object):
def __init__(self, chickens=0, wolves=0, has_boat=False):
self.chickens = chickens
self.wolves = wolves
self.has_boat = has_boat
def __eq__(self, other):
chickens_eq = self.chickens == other.chickens
wolves_eq = self.wolves == other.wolves
has_boat_eq = self.has_boat == other.has_boat
return chickens_eq and wolves_eq and has_boat_eq
def __hash__(self):
return hash((self.chickens, self.wolves, self.has_boat))
def __str__(self):
return "Chickens: {}, Wolves: {}, Has Boat: {}".format(self.chickens, self.wolves, self.has_boat)
def __copy__(self):
return type(self)(self.chickens, self.wolves, self.has_boat)
def valid_state(self):
return (self.wolves <= self.chickens or self.chickens == 0) and (self.wolves >= 0) and (self.chickens >= 0)
class GameState(object):
AVAILABLE_ACTIONS = [
{"chickens": 1, "wolves": 0},
{"chickens": 2, "wolves": 0},
{"chickens": 0, "wolves": 1},
{"chickens": 1, "wolves": 1},
{"chickens": 0, "wolves": 2},
]
def __init__(self, left_bank=None, right_bank=None, total_chickens=0, total_wolves=0):
self.left_bank = BankState() if left_bank is None else left_bank
self.right_bank = BankState() if right_bank is None else right_bank
self.total_chickens = total_chickens
self.total_wolves = total_wolves
def __eq__(self, other):
return self.left_bank == other.left_bank and self.right_bank == other.right_bank
def __hash__(self):
return hash((self.left_bank, self.right_bank))
def __str__(self):
return "[Left Bank -> {} | Right Bank -> {}]".format(self.left_bank, self.right_bank)
def __copy__(self):
return type(self)(
copy(self.left_bank), copy(self.right_bank), copy(self.total_chickens), copy(self.total_wolves)
)
def valid_state(self):
banks_valid = self.left_bank.valid_state() and self.right_bank.valid_state()
chickens_valid = (self.left_bank.chickens + self.right_bank.chickens) == self.total_chickens
wolves_valid = (self.left_bank.wolves + self.right_bank.wolves) == self.total_wolves
return banks_valid and chickens_valid and wolves_valid
def apply_action(self, action, left_to_right_bank=True):
if left_to_right_bank:
self.right_bank.chickens += action["chickens"]
self.right_bank.wolves += action["wolves"]
self.right_bank.has_boat = True
self.left_bank.chickens -= action["chickens"]
self.left_bank.wolves -= action["wolves"]
self.left_bank.has_boat = False
else:
self.right_bank.chickens -= action["chickens"]
self.right_bank.wolves -= action["wolves"]
self.right_bank.has_boat = False
self.left_bank.chickens += action["chickens"]
self.left_bank.wolves += action["wolves"]
self.left_bank.has_boat = True
def set_from_file(self, file_path):
with open(file_path, "r") as game_state_file:
read_csv = list(csv.reader(game_state_file))
left_bank_state, right_bank_state = read_csv[0], read_csv[1]
self.left_bank.chickens = int(left_bank_state[0])
self.left_bank.wolves = int(left_bank_state[1])
self.left_bank.has_boat = left_bank_state[2] == "1"
self.right_bank.chickens = int(right_bank_state[0])
self.right_bank.wolves = int(right_bank_state[1])
self.right_bank.has_boat = right_bank_state[2] == "1"
self.total_chickens = self.left_bank.chickens + self.right_bank.chickens
self.total_wolves = self.left_bank.wolves + self.right_bank.wolves
class Node(object):
def __init__(self, parent_node, game_state, path_cost, depth):
self.parent_node = parent_node
self.game_state = game_state
self.depth = depth
self.path_cost = path_cost
def __str__(self):
return "{} | {} | {}".format(self.game_state, self.depth, self.path_cost)
def __hash__(self):
return hash((self.parent_node, self.game_state, self.depth, self.path_cost))
def __lt__(self, other):
return hash(self) < hash(other)
class GameSolver(object):
def __init__(self, start_state, goal_state):
self.start_state = start_state
self.goal_state = goal_state
self.expanded_nodes_count = 0
self.fringe = deque()
self.solution = None
def solution_string(self):
solutions_strings = deque()
sentinel = self.solution
while sentinel is not None:
solutions_strings.appendleft(str(sentinel.game_state))
sentinel = sentinel.parent_node
return "\n".join(solutions_strings)
def show_and_save_solution(self, file_path):
if solver.solution:
solution_string = self.solution_string()
print(solution_string)
with open(file_path, "w") as output_file:
output_file.write(solution_string)
output_file.write("\n")
print()
print("{} nodes were expanded for a solution with a depth of {}".format(
solver.expanded_nodes_count,
solver.solution.depth)
)
else:
print("No solution found in {} expanded nodes".format(solver.expanded_nodes_count))
@staticmethod
def valid_states_from_current_node(current_node):
valid_states = []
current_state = current_node.game_state
left_to_right_bank = current_state.left_bank.has_boat
for action in current_state.AVAILABLE_ACTIONS:
test_state = copy(current_state)
test_state.apply_action(action, left_to_right_bank)
if test_state.valid_state():
valid_states.append(test_state)
return valid_states
def is_goal(self, node):
return node.game_state == self.goal_state
def expand(self, node):
pass
def solve(self):
pass
class BFSGameSolver(GameSolver):
def expand(self, current_node):
successors = deque()
new_path_cost = current_node.path_cost + 1
new_depth = current_node.depth + 1
for state in self.valid_states_from_current_node(current_node):
successors.append(Node(current_node, state, new_path_cost, new_depth))
return successors
def solve(self):
closed = set()
self.fringe.append(Node(None, self.start_state, 0, 0))
while True:
if not self.fringe:
self.solution = None
return
current_node = self.fringe.popleft()
current_state = current_node.game_state
if self.is_goal(current_node):
self.solution = current_node
return
self.expanded_nodes_count += 1
if current_state not in closed:
closed.add(current_state)
self.fringe += self.expand(current_node)
class DFSGameSolver(GameSolver):
def expand(self, current_node):
successors = deque()
new_path_cost = current_node.path_cost + 1
new_depth = current_node.depth + 1
for state in reversed(self.valid_states_from_current_node(current_node)):
successors.append(Node(current_node, state, new_path_cost, new_depth))
return successors
def solve(self):
closed = set()
self.fringe.append(Node(None, self.start_state, 0, 0))
while True:
if not self.fringe:
self.solution = None
return
current_node = self.fringe.pop()
current_state = current_node.game_state
if self.is_goal(current_node):
self.solution = current_node
return
self.expanded_nodes_count += 1
if current_state not in closed:
closed.add(current_state)
self.fringe += self.expand(current_node)
class IDDFSGameSolver(GameSolver):
def __init__(self, start_state, goal_state, starting_depth=1):
super(IDDFSGameSolver, self).__init__(start_state, goal_state)
self.current_depth = starting_depth
self.max_depth = 1000000
def expand(self, current_node):
successors = deque()
if current_node.depth == self.current_depth:
return successors
new_path_cost = current_node.path_cost + 1
new_depth = current_node.depth + 1
for state in reversed(self.valid_states_from_current_node(current_node)):
successors.append(Node(current_node, state, new_path_cost, new_depth))
return successors
def solve(self):
closed = set()
start_state_node = Node(None, self.start_state, 0, 0)
self.fringe.append(start_state_node)
while True:
if not self.fringe:
if self.current_depth != self.max_depth:
self.current_depth += 1
closed = set()
self.fringe.append(Node(None, self.start_state, 0, 0))
else:
self.solution = None
return
current_node = self.fringe.pop()
current_state = current_node.game_state
if self.is_goal(current_node):
self.solution = current_node
return
self.expanded_nodes_count += 1
if current_state not in closed:
closed.add(current_state)
self.fringe += self.expand(current_node)
class AStarGameSolver(GameSolver):
def __init__(self, start_state, goal_state):
super(AStarGameSolver, self).__init__(start_state, goal_state)
self.fringe = PriorityQueue()
self.left_to_right_chickens = self.goal_state.left_bank.chickens < self.goal_state.right_bank.chickens
self.left_to_right_wolves = self.goal_state.left_bank.wolves < self.goal_state.right_bank.wolves
def astar_heuristic(self, current_node):
current_state = current_node.game_state
if self.left_to_right_chickens:
min_moves_chickens = (self.goal_state.right_bank.chickens - current_state.right_bank.chickens) / 2.0
else:
min_moves_chickens = (self.goal_state.left_bank.chickens - current_state.left_bank.chickens) / 2.0
if self.left_to_right_wolves:
min_moves_wolves = (self.goal_state.right_bank.wolves - current_state.right_bank.wolves) / 2.0
else:
min_moves_wolves = (self.goal_state.left_bank.wolves - current_state.left_bank.wolves) / 2.0
return min_moves_chickens + min_moves_wolves
def astar_evaluator(self, current_node):
return self.astar_heuristic(current_node) + current_node.path_cost
def expand(self, current_node):
new_path_cost = current_node.path_cost + 1
new_depth = current_node.depth + 1
for state in self.valid_states_from_current_node(current_node):
new_node = Node(current_node, state, new_path_cost, new_depth)
evaluated_priority = self.astar_evaluator(new_node)
self.fringe.put((evaluated_priority, new_node))
def solve(self):
closed = set()
self.fringe.put((1, Node(None, self.start_state, 0, 0)))
while True:
if not self.fringe:
self.solution = None
return
_, current_node = self.fringe.get()
current_state = current_node.game_state
if self.is_goal(current_node):
self.solution = current_node
return
self.expanded_nodes_count += 1
if current_state not in closed:
closed.add(current_state)
self.expand(current_node)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Solves the chicken and wolves game")
parser.add_argument("initial_state_file", help="Filename of the state file in the current directory")
parser.add_argument("goal_state_file", help="Filename of the goal state file in the current directory")
parser.add_argument("mode", choices=["bfs", "dfs", "iddfs", "astar"], help="Mode to run the solver with")
parser.add_argument("output_file", help="Desired filename of the output file in the current directory")
args = parser.parse_args()
print("Starting chickens and wolves game solver\n")
sys.setrecursionlimit(10000)
starting_state = GameState()
ending_state = GameState()
starting_state.set_from_file(args.initial_state_file)
ending_state.set_from_file(args.goal_state_file)
if args.mode == "bfs":
print("Running bfs on start_state {} with goal {}".format(starting_state, ending_state))
solver = BFSGameSolver(starting_state, ending_state)
elif args.mode == "dfs":
print("Running dfs on start_state {} with goal {}".format(starting_state, ending_state))
solver = DFSGameSolver(starting_state, ending_state)
elif args.mode == "iddfs":
print("Running iddfs on start_state {} with goal {}".format(starting_state, ending_state))
solver = IDDFSGameSolver(starting_state, ending_state)
else:
print("Running astar on start_state {} with goal {}".format(starting_state, ending_state))
solver = AStarGameSolver(starting_state, ending_state)
print()
start_time = time()
solver.solve()
end_time = time() - start_time
solver.show_and_save_solution(args.output_file)
print("Solve ran in {} seconds".format(end_time))