first commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
env/
|
||||||
|
.vscode/
|
||||||
|
__pycache__/
|
60
exceptions.py
Normal file
60
exceptions.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
"""Exceptions used for the vial solver."""
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
class InvalidVial(Exception):
|
||||||
|
"""Raised when an invalid vial is initialized."""
|
||||||
|
def __init__(self, balls, size):
|
||||||
|
self.message = "The vial you tried to initialize is not possible."
|
||||||
|
self.message += f"\n(balls: {balls}, size: {size})"
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
class InvalidGame(Exception):
|
||||||
|
"""Raised when an invalid game is initialized."""
|
||||||
|
def __init__(self, vial_string):
|
||||||
|
self.message = "The game you tried to initialize is not possible."
|
||||||
|
self.message += f"\n(input string: {vial_string})"
|
||||||
|
|
||||||
|
count = Counter(list(vial_string.replace(" ", "")))
|
||||||
|
count_counter = Counter(list(count.values()))
|
||||||
|
if 1 in count_counter.values():
|
||||||
|
wrong_letter = list(count.keys())[
|
||||||
|
list(count.values()).index(list(count_counter.keys())[
|
||||||
|
list(count_counter.values()).index(1)
|
||||||
|
])
|
||||||
|
]
|
||||||
|
pointers = ' '*len("(input string: ")
|
||||||
|
indices = [index for index, value in enumerate(list(vial_string))
|
||||||
|
if value == wrong_letter]
|
||||||
|
for i, index in enumerate(indices):
|
||||||
|
if i > 0:
|
||||||
|
index -= indices[i-1]+1
|
||||||
|
|
||||||
|
pointers += ' '*index + "^"
|
||||||
|
self.message += "\n"+pointers
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
class NoSolutions(Exception):
|
||||||
|
"""Raised when an invalid game is initialized."""
|
||||||
|
def __init__(self):
|
||||||
|
self.message = "There are no valid solutions for the game."
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
class VialFull(Exception):
|
||||||
|
"""Raised when a full vial is pushed to."""
|
||||||
|
def __init__(self, balls, size):
|
||||||
|
self.message = "The vial you tried to push to is already full."
|
||||||
|
self.message += f"\n(balls: {balls}, size: {size})"
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
class VialEmpty(Exception):
|
||||||
|
"""Raised when an empty vial is popped."""
|
||||||
|
def __init__(self, balls, size):
|
||||||
|
self.message = "The vial you tried to pull from is empty."
|
||||||
|
self.message += f"\n(balls: {balls}, size: {size})"
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
class HeapEmpty(Exception):
|
||||||
|
"""Raised when extract_min() is tried on an empty heap."""
|
||||||
|
def __init__(self):
|
||||||
|
self.message = "The heap you tried to extract from is empty."
|
||||||
|
super().__init__(self.message)
|
90
fibonacci_heap.py
Normal file
90
fibonacci_heap.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
"""An implementation of a fibonacci heap."""
|
||||||
|
from __future__ import annotations
|
||||||
|
from math import log2, ceil
|
||||||
|
|
||||||
|
import exceptions
|
||||||
|
|
||||||
|
class FibonacciNode():
|
||||||
|
"""A node for the fibonacci heap."""
|
||||||
|
def __init__(self, element, value):
|
||||||
|
self.element = element
|
||||||
|
self.value = value
|
||||||
|
self.children = []
|
||||||
|
self.size = 1
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self.size
|
||||||
|
|
||||||
|
def __lt__(self, other: FibonacciNode):
|
||||||
|
return self.value < other.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def degree(self):
|
||||||
|
"""Return the degree (number of children) of the node."""
|
||||||
|
return len(self.children)
|
||||||
|
|
||||||
|
def add_child(self, child: FibonacciNode):
|
||||||
|
"""Adds a child to the node."""
|
||||||
|
self.children.append(child)
|
||||||
|
self.size += child.size
|
||||||
|
|
||||||
|
class FibonacciHeap():
|
||||||
|
"""The fibonacci heap."""
|
||||||
|
def __init__(self, element, value: int):
|
||||||
|
new_node = FibonacciNode(element, value)
|
||||||
|
self.roots = [new_node]
|
||||||
|
self.min = new_node
|
||||||
|
self.size = 1
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self.size
|
||||||
|
|
||||||
|
def merge(self, other: FibonacciHeap):
|
||||||
|
"""Merge two fibonacci heaps."""
|
||||||
|
self.roots += other.roots
|
||||||
|
self.size += other.size
|
||||||
|
if self.min is None or other.min < self.min:
|
||||||
|
self.min = other.min
|
||||||
|
|
||||||
|
def insert(self, element, value: int):
|
||||||
|
"""Insert an element into the heap."""
|
||||||
|
self.merge(FibonacciHeap(element, value))
|
||||||
|
|
||||||
|
def extract_min(self):
|
||||||
|
"""Extract the element with the smallest value."""
|
||||||
|
if self.min is None:
|
||||||
|
raise exceptions.HeapEmpty()
|
||||||
|
element = self.min.element
|
||||||
|
value = self.min.value
|
||||||
|
self.roots += self.min.children
|
||||||
|
self.roots.remove(self.min)
|
||||||
|
self.size -= 1
|
||||||
|
if self.roots == []:
|
||||||
|
self.min = None
|
||||||
|
else:
|
||||||
|
self._rearrange()
|
||||||
|
self.min = min(self.roots)
|
||||||
|
|
||||||
|
return element, value
|
||||||
|
|
||||||
|
def _rearrange(self):
|
||||||
|
heap_size = len(self)
|
||||||
|
required_roots = (0 if heap_size == 0 else
|
||||||
|
1 if heap_size == 1 else
|
||||||
|
ceil(log2(heap_size)))
|
||||||
|
|
||||||
|
while len(self.roots) > required_roots:
|
||||||
|
degree_array = [None for _ in range(required_roots)]
|
||||||
|
for root in self.roots:
|
||||||
|
root_degree = root.degree
|
||||||
|
if degree_array[root_degree] is None:
|
||||||
|
degree_array[root_degree] = root
|
||||||
|
else:
|
||||||
|
if degree_array[root_degree] < root:
|
||||||
|
degree_array[root_degree].add_child(root)
|
||||||
|
self.roots.remove(root)
|
||||||
|
else:
|
||||||
|
root.add_child(degree_array[root_degree])
|
||||||
|
self.roots.remove(degree_array[root_degree])
|
||||||
|
|
||||||
|
break
|
29
main.py
Normal file
29
main.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"""Main module."""
|
||||||
|
import time
|
||||||
|
from vial_game import Game
|
||||||
|
from vial_solver import solve_game, format_instructions
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""The main method for the vial solver."""
|
||||||
|
colors = [(75, "c"), (215, "o"), (130, "b"), (245, "g"), (226, "y"),
|
||||||
|
(77, "l"), (90, "u"), (70, "m"), (198, "i"), (20, "e"),
|
||||||
|
(160, "r"), (22, "d")]
|
||||||
|
for color, letter in colors:
|
||||||
|
print(f" \033[38;5;{color}m⚫\033[39m = {letter} |", end="")
|
||||||
|
print("\b")
|
||||||
|
try:
|
||||||
|
vial_string = input()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print()
|
||||||
|
return
|
||||||
|
game = Game(vial_string)
|
||||||
|
print(game)
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
solution = solve_game(game)
|
||||||
|
print(f"\rFound solution with {len(solution)} steps.")
|
||||||
|
print(format_instructions(solution, start))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
144
vial_game.py
Normal file
144
vial_game.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
"""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)]
|
92
vial_solver.py
Normal file
92
vial_solver.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
"""A solver for a vial-type game."""
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
from fibonacci_heap import FibonacciHeap
|
||||||
|
from vial_game import Game
|
||||||
|
from exceptions import NoSolutions
|
||||||
|
|
||||||
|
def print_progress(amount_done):
|
||||||
|
extra_symbol = ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'][
|
||||||
|
math.floor(amount_done * 160) % 8
|
||||||
|
]
|
||||||
|
|
||||||
|
progress_bar = "\r|{}{}{}| {}% ".format(
|
||||||
|
'█'*math.floor(amount_done*20),
|
||||||
|
extra_symbol,
|
||||||
|
' '*(math.ceil((1-amount_done)*20)-1),
|
||||||
|
math.floor(amount_done*100)
|
||||||
|
)
|
||||||
|
print(progress_bar, end="")
|
||||||
|
|
||||||
|
|
||||||
|
def solve_game(starting_state: Game):
|
||||||
|
"""Solves a vial game."""
|
||||||
|
if starting_state.is_solved:
|
||||||
|
return []
|
||||||
|
|
||||||
|
attempted_states = {starting_state.vial_string(): []}
|
||||||
|
states = FibonacciHeap((starting_state, []), starting_state.min_moves)
|
||||||
|
amount_done = 0
|
||||||
|
moves_amount = -1
|
||||||
|
|
||||||
|
while len(states) > 0:
|
||||||
|
(current_state, moves), value = states.extract_min()
|
||||||
|
if len(moves) > moves_amount:
|
||||||
|
moves_amount = len(moves)
|
||||||
|
print(f"{len(states) + 1} ({len(attempted_states)})")
|
||||||
|
|
||||||
|
if amount_done + 0.00125 < len(moves)/value:
|
||||||
|
amount_done = (len(moves)/value)
|
||||||
|
print_progress(amount_done)
|
||||||
|
|
||||||
|
last_to_vial = moves[-1][1] if len(moves) > 1 else None
|
||||||
|
possible_moves = current_state.possible_moves(last_to_vial)
|
||||||
|
for from_vial, to_vial in possible_moves:
|
||||||
|
new_state = current_state.copy()
|
||||||
|
new_state.move(from_vial, to_vial)
|
||||||
|
if new_state.vials[to_vial].is_solved:
|
||||||
|
star = "*"
|
||||||
|
else:
|
||||||
|
star = " "
|
||||||
|
new_state_moves = moves + [(from_vial, to_vial, star)]
|
||||||
|
if new_state.is_solved:
|
||||||
|
return new_state_moves
|
||||||
|
|
||||||
|
if new_state.vial_string() not in attempted_states:
|
||||||
|
attempted_states[new_state.vial_string()] = new_state_moves
|
||||||
|
new_state_value = new_state.min_moves + len(new_state_moves)
|
||||||
|
states.insert((new_state, new_state_moves), new_state_value)
|
||||||
|
|
||||||
|
print("\r", end="")
|
||||||
|
raise NoSolutions()
|
||||||
|
|
||||||
|
def format_instructions(instructions, start_time):
|
||||||
|
"""Format instructions outputted by solve_game()."""
|
||||||
|
time_in_milliseconds = math.floor((time.time()-start_time)*1000)
|
||||||
|
time_text = ""
|
||||||
|
if time_in_milliseconds >= (1000*60):
|
||||||
|
time_in_minutes = str(math.floor(time_in_milliseconds/(1000*60)))
|
||||||
|
time_text += time_in_minutes+"m "
|
||||||
|
if time_in_milliseconds >= 1000:
|
||||||
|
time_in_seconds = str(math.floor(time_in_milliseconds/1000)%60)
|
||||||
|
time_text += time_in_seconds+"s "
|
||||||
|
|
||||||
|
time_text += str(time_in_milliseconds%1000)+"ms "
|
||||||
|
|
||||||
|
formatted_instructions = f"Time elapsed: {time_text}\n"
|
||||||
|
formatted_instructions += "Instructions:\n"
|
||||||
|
for i, instruction in enumerate(instructions):
|
||||||
|
if i%7 == 0 and i != 0:
|
||||||
|
formatted_instructions += "\n"
|
||||||
|
if i%21 == 0 and i != 0:
|
||||||
|
formatted_instructions += "\n"
|
||||||
|
if i%63 == 0 and i != 0:
|
||||||
|
formatted_instructions += "\n\n"
|
||||||
|
|
||||||
|
from_vial = instruction[0]
|
||||||
|
to_vial = instruction[1]
|
||||||
|
star = instruction[2]
|
||||||
|
step = f" {star}{from_vial:2} --> {to_vial:2}{star} \t"
|
||||||
|
formatted_instructions += step
|
||||||
|
|
||||||
|
return formatted_instructions
|
Reference in New Issue
Block a user