Files
Vial-Game-Solver/vial_game.py
2022-02-11 13:17:15 +01:00

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