nus/cs2109s/labs/ps2/cube.py
2024-02-08 00:40:44 +08:00

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