145 lines
4.8 KiB
Python
145 lines
4.8 KiB
Python
"""Implementation of the vial game."""
|
|
from itertools import product
|
|
from collections import Counter
|
|
import exceptions
|
|
|
|
def _valid_game(vial_string):
|
|
count = Counter(list(vial_string.replace(" ", "")))
|
|
return len(set(count[i] for i in count if not i.isnumeric())) == 1
|
|
|
|
class Vial():
|
|
"""Implementation of a vial."""
|
|
def __init__(self, balls: list[str], size: int):
|
|
if len(balls) > size:
|
|
raise exceptions.InvalidVial(balls, size)
|
|
self.balls = balls
|
|
self.size = size
|
|
|
|
def __repr__(self):
|
|
return "".join(self.balls) + "-"*(self.size-len(self.balls))
|
|
|
|
def __len__(self):
|
|
return len(self.balls)
|
|
|
|
@property
|
|
def is_full(self):
|
|
"""Test if the vial is full."""
|
|
return len(self.balls) >= self.size
|
|
|
|
@property
|
|
def is_empty(self):
|
|
"""Test if the vial is empty."""
|
|
return len(self.balls) == 0
|
|
|
|
@property
|
|
def is_solved(self):
|
|
"""Test if the vial is solved."""
|
|
return self.is_empty or (
|
|
self.is_full and all(i == self.balls[0] for i in self.balls))
|
|
|
|
@property
|
|
def is_semi_solved(self):
|
|
"""Test if the vial is solved."""
|
|
return (all(i == self.balls[0] for i in self.balls) and
|
|
len(self.balls) > self.size//2)
|
|
|
|
@property
|
|
def top_ball(self):
|
|
"""Get the top ball of the vial."""
|
|
return self.balls[-1] or None
|
|
|
|
@property
|
|
def min_moves(self):
|
|
"""get the minimum amount of moves to solve the vial."""
|
|
return len([i for x, i in enumerate((self.balls)) if
|
|
any(i != self.balls[j] for j in range(x))])
|
|
|
|
def copy(self):
|
|
""""Return a copy of itself."""
|
|
return Vial(self.balls.copy(), self.size)
|
|
|
|
def push(self, ball: str):
|
|
"""Add ball to the vial."""
|
|
if self.is_full:
|
|
raise exceptions.VialFull(self.balls, self.size)
|
|
self.balls += [ball]
|
|
|
|
def pull(self):
|
|
"""Remove the top ball."""
|
|
if self.is_empty:
|
|
raise exceptions.VialEmpty(self.balls, self.size)
|
|
return self.balls.pop()
|
|
|
|
class Game():
|
|
"""Implementation of a full game of vials."""
|
|
def __init__(self, vial_string: str):
|
|
if not (vial_string == "" or _valid_game(vial_string)):
|
|
raise exceptions.InvalidGame(vial_string)
|
|
balls = [list(i) for i in vial_string.split(" ") if not i.isnumeric()]
|
|
balls += [
|
|
[] for _ in range(
|
|
sum([int(i) for i in vial_string.split(" ") if i.isnumeric()]))
|
|
]
|
|
vial_size = max([len(i) for i in balls])
|
|
self.vials = [Vial(i[::-1], vial_size) for i in balls]
|
|
|
|
def __repr__(self):
|
|
return "|".join([repr(i) for i in self.vials])
|
|
|
|
def vial_string(self):
|
|
"""Represents the game as a string, ignoring vial order."""
|
|
return "|".join(sorted([repr(i) for i in self.vials]))
|
|
|
|
@property
|
|
def is_solved(self):
|
|
"""Test if the game is solved."""
|
|
return all(i.is_solved for i in self.vials)
|
|
|
|
@property
|
|
def possible_from_vials(self):
|
|
"""Return a list of indexes for non-empty vials."""
|
|
return [i for i, vial in
|
|
enumerate(self.vials) if (not
|
|
(vial.is_empty or
|
|
vial.is_semi_solved))]
|
|
|
|
@property
|
|
def possible_to_vials(self):
|
|
"""Return a list of indexes for non-full vials."""
|
|
return [i for i, vial in enumerate(self.vials) if not vial.is_full]
|
|
|
|
@property
|
|
def min_moves(self):
|
|
"""Returns the minimum amount of moves to solve the game."""
|
|
if self.is_solved:
|
|
return 0
|
|
|
|
return max(sum(i.min_moves for i in self.vials), 1)
|
|
|
|
def copy(self):
|
|
"""Return of copy of itself."""
|
|
game_copy = Game("")
|
|
game_copy.vials = [i.copy() for i in self.vials]
|
|
return game_copy
|
|
|
|
def move(self, from_vial: int, to_vial: int):
|
|
"""Move the top ball from one vial to another."""
|
|
self.vials[to_vial].push(self.vials[from_vial].pull())
|
|
|
|
def compatible(self, from_vial: int, to_vial: int):
|
|
"""Test if you can move the top ball from one vial to another."""
|
|
from_vial, to_vial = (self.vials[i] for i in [from_vial, to_vial])
|
|
return ((not from_vial.is_empty) and
|
|
(to_vial.is_empty or
|
|
from_vial.top_ball == to_vial.top_ball))
|
|
|
|
def possible_moves(self, last_to_vial):
|
|
"""Generate all possible moves."""
|
|
possible_from_vials = self.possible_from_vials
|
|
possible_to_vials = self.possible_to_vials
|
|
if last_to_vial and last_to_vial in possible_from_vials:
|
|
possible_from_vials.remove(last_to_vial)
|
|
|
|
move_list = product(possible_from_vials, possible_to_vials)
|
|
return [i for i in move_list if self.compatible(*i)]
|