"""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)]