296 lines
9.4 KiB
Python
296 lines
9.4 KiB
Python
"""
|
|
A Rubik's Cube like this
|
|
0 1 2
|
|
|-----|-----|-----|
|
|
0 | R | R | R |
|
|
|-----|-----|-----|
|
|
1 | G | G | G |
|
|
|-----|-----|-----|
|
|
2 | B | B | B |
|
|
|-----|-----|-----|
|
|
"""
|
|
import copy
|
|
import json
|
|
from ast import literal_eval
|
|
from typing import Dict, Iterable, List, Optional, Tuple, Union
|
|
|
|
Action = List[Union[int, str]]
|
|
|
|
class State:
|
|
r"""State class describes the setting of the Cube
|
|
|
|
Args:
|
|
shape (List[int]): describe the number of rows and columns of the cube
|
|
layout (Iterable[int]): describe the layout of the cube. The length of
|
|
layout should be equal to the product of shape.
|
|
|
|
Example:
|
|
State([2,3],[0, 1, 2, 3, 4, 5]) represents the state of
|
|
label: 0 1 2
|
|
0 | 0 | 1 | 2 |
|
|
1 | 3 | 4 | 5 |
|
|
|
|
Methods:
|
|
left(label): move the @label row left
|
|
returns the copy of new state (State)
|
|
|
|
right(label): move the @label row right
|
|
returns the copy of new state (State)
|
|
|
|
up(label): move the @label col up
|
|
returns the copy of new state (State)
|
|
|
|
down(label): move the @label col down
|
|
returns the copy of new state (State)
|
|
"""
|
|
|
|
def __init__(self, shape: Tuple[int,int], layout: Iterable[int]):
|
|
if len(layout) != shape[0]*shape[1]:
|
|
raise ValueError("layout does not match the shape")
|
|
self.__shape = list(shape)
|
|
self.__layout = layout
|
|
|
|
def __eq__(self, state: "State"):
|
|
if isinstance(state, State):
|
|
same_shape = state.shape[0] == self.__shape[0] and \
|
|
state.shape[1] == self.__shape[1]
|
|
same_layout = all([x==y for x,y in zip(self.__layout,state.layout)])
|
|
return same_shape and same_layout
|
|
else:
|
|
return False
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(tuple(self.__layout))
|
|
|
|
def __repr__(self) -> str:
|
|
return str({'shape': self.__shape, 'layout': self.__layout})
|
|
|
|
def __str__(self):
|
|
# Header
|
|
row_str = f"{' '*5} "
|
|
for col in range(self.shape[1]):
|
|
row_str += f"{col:^5d} "
|
|
cube_str = row_str + '\n'
|
|
cube_str += f"{' '*5}+{'-----+'*self.shape[1]}\n"
|
|
# Content
|
|
for row in range(self.shape[0]):
|
|
row_str = f"{row:^5d}|"
|
|
for col in range(self.shape[1]):
|
|
row_str += f"{str(self.layout[row*self.shape[1]+col]):^5s}|"
|
|
cube_str += row_str + '\n'
|
|
cube_str += f"{' '*5}+{'-----+'*self.shape[1]}\n"
|
|
|
|
return cube_str
|
|
|
|
|
|
@property
|
|
def shape(self):
|
|
return copy.deepcopy(self.__shape)
|
|
|
|
@property
|
|
def layout(self):
|
|
return copy.deepcopy(self.__layout)
|
|
|
|
def left(self, label):
|
|
layout = self.layout
|
|
rows, cols = self.shape
|
|
head = layout[label * cols]
|
|
for i in range(cols-1):
|
|
layout[label * cols + i] = layout[label * cols + i + 1]
|
|
layout[(label+1) * cols - 1] = head
|
|
return State(self.shape,layout)
|
|
|
|
def right(self, label):
|
|
layout = self.layout
|
|
rows, cols = self.shape
|
|
tail = layout[(label + 1) * cols - 1]
|
|
for i in range(cols - 1, 0, -1):
|
|
layout[label * cols + i] = layout[label * cols + i - 1]
|
|
layout[label * cols] = tail
|
|
return State(self.shape,layout)
|
|
|
|
def up(self, label):
|
|
layout = self.layout
|
|
rows, cols = self.shape
|
|
head = layout[label]
|
|
for i in range(rows-1):
|
|
layout[label + cols * i] = layout[label + cols * (i + 1)]
|
|
layout[label + cols * (rows - 1)] = head
|
|
return State(self.shape,layout)
|
|
|
|
def down(self, label):
|
|
layout = self.layout
|
|
rows, cols = self.shape
|
|
tail = layout[label + cols * (rows - 1)]
|
|
for i in range(rows - 1, 0, -1):
|
|
layout[label + cols * i] = layout[label + cols * (i - 1)]
|
|
layout[label] = tail
|
|
return State(self.shape,layout)
|
|
|
|
|
|
class Cube:
|
|
r"""Cube problem class
|
|
Args:
|
|
input_file (Optional[str]): the absolute path of the Cube json file
|
|
initial (Optional[State]): the initial state of the Cube
|
|
goal (Union[State, Iterable[State]]): the goal
|
|
state(s) of the cube.
|
|
"""
|
|
def __init__(
|
|
self,
|
|
input_file: Optional[str] = None,
|
|
input_dict: Optional[Dict] = None,
|
|
initial: Optional[State] = None,
|
|
goal:Optional[State] = None
|
|
):
|
|
if input_file:
|
|
with open(input_file, 'r') as f:
|
|
data = json.load(f)
|
|
state_dict = literal_eval(data['initial'])
|
|
self.__initial = State(state_dict['shape'],state_dict['layout'])
|
|
state_dict = literal_eval(data['goal'])
|
|
self.__goal = State(state_dict['shape'],state_dict['layout'])
|
|
|
|
elif input_dict:
|
|
state_dict = input_dict['initial']
|
|
self.__initial = State(state_dict['shape'],state_dict['layout'])
|
|
state_dict = input_dict['goal']
|
|
self.__goal = State(state_dict['shape'],state_dict['layout'])
|
|
|
|
elif all([initial, goal]):
|
|
self.__initial = initial
|
|
self.__goal = goal
|
|
|
|
else:
|
|
raise ValueError
|
|
|
|
self.__actions = self._get_actions(*self.__initial.shape)
|
|
|
|
def __repr__(self) -> str:
|
|
return repr({'initial':repr(self.__initial), 'goal':repr(self.__goal)})
|
|
|
|
def __str__(self) -> str:
|
|
return f"initial:\n{str(self.__initial)}\ngoal:\n{str(self.__goal)}"
|
|
|
|
def _get_actions(self, rows:int, cols:int):
|
|
actions = []
|
|
for i in range(rows):
|
|
actions.append([i,"left"])
|
|
actions.append([i,"right"])
|
|
for i in range(cols):
|
|
actions.append([i,"up"])
|
|
actions.append([i,"down"])
|
|
return actions
|
|
|
|
|
|
# Observable Environment
|
|
@property
|
|
def initial(self):
|
|
return copy.deepcopy(self.__initial)
|
|
|
|
@property
|
|
def goal(self):
|
|
return copy.deepcopy(self.__goal)
|
|
|
|
def actions(self, state: State):
|
|
r"""Return the actions that can be executed in the given state.
|
|
|
|
Args:
|
|
state (State): the state to be checked for actions.
|
|
|
|
Returns:
|
|
A list of actions can be executed at the provided state.
|
|
"""
|
|
return copy.deepcopy(self.__actions)
|
|
|
|
# Transition Model (Deterministic)
|
|
def result(self, source: State, action):
|
|
r"""Return the state that results from executing the given
|
|
action in the given state. The action must be one of
|
|
self.actions(state).
|
|
|
|
Args:
|
|
source (State): the state to excute the action
|
|
action: the action can be executed
|
|
|
|
Returns:
|
|
the state after taking action from source
|
|
"""
|
|
assert list(action) in self.actions(source), \
|
|
f"{action} is illegal action at {source}"
|
|
|
|
label, act = action
|
|
if act == 'left':
|
|
result = source.left(label)
|
|
elif act == 'right':
|
|
result = source.right(label)
|
|
elif act == 'down':
|
|
result = source.down(label)
|
|
elif act == 'up':
|
|
result = source.up(label)
|
|
return result
|
|
|
|
def path_cost(self, c: float, state1: State, action,
|
|
state2: State) -> float:
|
|
r"""Return the cost of a solution path that arrives at state2 from
|
|
state1 via action, assuming cost c to get up to state1.
|
|
|
|
.. math::
|
|
c + action cost
|
|
|
|
Args:
|
|
c (float): the cost of getting state1 from the initial state
|
|
state1 (State): the State before executing action
|
|
action: the action taken at state1
|
|
state2 (State): the State after executing action
|
|
|
|
Returns:
|
|
the path cost of reaching state2
|
|
"""
|
|
if self.result(state1, action) == state2:
|
|
return c + 1
|
|
|
|
# Goal Test
|
|
def goal_test(self, state: State) -> bool:
|
|
r"""Return True if the state is a goal. The default method compares the
|
|
state to self.goal or checks for state in self.goal if it is a
|
|
list, as specified in the constructor.
|
|
|
|
Args:
|
|
state (State): the state to be checked
|
|
|
|
Return:
|
|
True if the give state is the goal state, otherwise False
|
|
"""
|
|
if isinstance(self.__goal, list):
|
|
return any(state == x for x in self.__goal)
|
|
else:
|
|
return state == self.__goal
|
|
|
|
# Solution Check
|
|
def verify_solution(self, solution, _print=False):
|
|
r"""Verify whether the given solution can reach goal state
|
|
|
|
Args:
|
|
solution (List): the list of actions is supposed to reach
|
|
goal state
|
|
|
|
Returns:
|
|
(True, cost) if the solution can reach the goal state,
|
|
(False, cost) if the solution fails to reach the goal state.
|
|
|
|
Notes:
|
|
cost == 0 means that there exists an illegal action in the solution
|
|
"""
|
|
curr = self.__initial
|
|
cost = 0
|
|
for action in solution:
|
|
if _print:
|
|
print(curr, action)
|
|
if list(action) not in self.actions(curr):
|
|
return False, 0
|
|
next = self.result(curr, action)
|
|
cost = self.path_cost(cost, curr, action, next)
|
|
curr = next
|
|
return self.goal_test(curr), cost
|