commit 47f2989373bcd749fe5434205731354e58533211 Author: omri Date: Thu Aug 22 16:19:27 2024 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d645c73 --- /dev/null +++ b/.gitignore @@ -0,0 +1,165 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +output.json +prune_table.pkl diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/__main__.py b/__main__.py new file mode 100644 index 0000000..1dbf15e --- /dev/null +++ b/__main__.py @@ -0,0 +1,128 @@ +from collections import defaultdict +import json +import os +import pickle +import sys +from eolr import ( + eolrb_states_generator, + EOLROrientation, + EOLRPermutation, + EOLR_PERMUTATIONS, +) +import humanize +from dataclasses import dataclass +from solver import EOLRBSolution, solve_eolrb, create_prune_table +from scorer import ( + FingerTrickWithRegrip, + load_config, + generate_finger_tricks, + build_pretty_string_from_finger_tricks_with_regrips, +) +import heapq +from typing import List, Any, Generator, Dict + +SOLUTIONS_TO_EVAL = 40 +SOLUTIONS_TO_SHOW = 3 + +PRUNE = 10 +SOLVE = 7 + + +@dataclass +class EOLRBSolutionWithFingertricks: + solution: EOLRBSolution + finger_tricks: List[FingerTrickWithRegrip] + + +def get_first_n_from_generator(gen: Generator[Any, None, Any], n: int) -> List[Any]: + results: List[Any] = [] + try: + while len(results) < n: + item = next(gen) + results.append(item) + except StopIteration: + pass + return results + + +def load_or_generate_prune_table(file_path: str, prune_size: int) -> Dict: + if os.path.isfile(file_path): + print("opening prune table from", file_path) + with open(file_path, "rb") as file: + return pickle.load(file) + else: + print("generating prune table...") + prune_table = create_prune_table(prune_size) + print("saving prune table to", file_path) + with open(file_path, "wb") as file: + pickle.dump(prune_table, file) + return prune_table + + +def main(): + config = load_config("./moves.yaml") + + prune_table = load_or_generate_prune_table("prune_table.pkl", PRUNE) + print("prune table size:", humanize.naturalsize(sys.getsizeof(prune_table))) + + # TODO: make this function a method of an object that contains a list of finger tricks + score_func = lambda solution: sum(ft.score() for ft in solution.finger_tricks) + data = defaultdict(list) + eolrb_states, n_states = eolrb_states_generator( + list(EOLROrientation), EOLR_PERMUTATIONS + ) + # eolrb_states, n_states = eolrb_states_generator( + # [EOLROrientation.Solved], [EOLRPermutation(UR="UR", UL="UL")] + # ) + for i, (eolrb_cube, ori, perm, pre_auf) in enumerate(eolrb_states): + print( + f"generating algs for {ori.name} (UR in {perm.UR}, UL in {perm.UL}, pre AUF {pre_auf}) ({i}/{n_states})" + ) + alg_generator = solve_eolrb(eolrb_cube, prune_table, PRUNE) + solutions = get_first_n_from_generator(alg_generator, SOLUTIONS_TO_EVAL) + solutions_with_finger_tricks = [ + EOLRBSolutionWithFingertricks( + solution, generate_finger_tricks(config, solution.alg) + ) + for solution in solutions + ] + best_solutions = heapq.nsmallest( + min(SOLUTIONS_TO_SHOW, len(solutions_with_finger_tricks)), + solutions_with_finger_tricks, + key=score_func, + ) + for solution_with_finger_tricks in best_solutions: + solution = solution_with_finger_tricks.solution + finger_tricks = solution_with_finger_tricks.finger_tricks + + pretty_alg = [] + if solution.pre_auf: + pretty_alg.append(solution.pre_auf) + pretty_alg.extend(solution.alg) + if solution.post_auf: + pretty_alg.append(solution.post_auf) + if solution.mc: + pretty_alg.append("M") + pretty_finger_tricks = build_pretty_string_from_finger_tricks_with_regrips( + finger_tricks + ) + + score = score_func(solution_with_finger_tricks) + alg_name = f"{ori.name} {perm.UR} {perm.UL} {pre_auf}" + data[alg_name].append( + { + "alg": " ".join(pretty_alg), + "stm": len(solution.alg), + "score": score, + "raw_alg": " ".join(solution.alg), + "pre_auf": solution.pre_auf, + "post_auf": solution.post_auf, + "finger_tricks": pretty_finger_tricks, + } + ) + with open("output.json", "w") as f: + f.write(json.dumps(data)) + + +if __name__ == "__main__": + main() diff --git a/cube.py b/cube.py new file mode 100644 index 0000000..8d8e0cc --- /dev/null +++ b/cube.py @@ -0,0 +1,205 @@ +from dataclasses import dataclass +from typing import Dict, List, Tuple +import hashlib + +M_SLICE_EDGES = ["UB", "UF", "DF", "DB"] +U_EDGES = ["UB", "UR", "UF", "UL"] +CENTERS = ["U", "F", "D", "B"] +U_CORNERS = ["UBR", "UFR", "UFL", "UBL"] + + +def cyclic_shift(d: Dict, keys: List, offset: int): + if not all(key in d for key in keys): + raise ValueError("some keys are missing from the dictionary") + values = [d[key] for key in keys] + effective_offset = offset % len(keys) + shifted_values = values[effective_offset:] + values[:effective_offset] + for key, value in zip(keys, shifted_values): + d[key] = value + + +@dataclass(unsafe_hash=True) +class Edge: + name: str + oriented: bool + + +class LSECube: + def __init__(self): + self.reset() + + def reset(self): + # self.edges = { + # "UB": Edge("UB", True), + # "UR": Edge("UR", True), + # "UF": Edge("UF", True), + # "UL": Edge("UL", True), + # "DB": Edge("DB", True), + # "DF": Edge("DF", True), + # } + self.edges = { + "UB": Edge("", True), + "UR": Edge("UR", True), + "UF": Edge("", True), + "UL": Edge("UL", True), + "DB": Edge("", True), + "DF": Edge("", True), + } + self.centers = { + "U": "U", + "B": "B", + "F": "F", + "D": "D", + } + self.corners = { + "UBR": "UBR", + "UFR": "UFR", + "UFL": "UFL", + "UBL": "UBL", + } + + def __hash__(self): + # TODO: I fucking hate this language. why can't hashing be simpelr + hash_dict = lambda d: hashlib.md5(str(sorted(d.items())).encode()).hexdigest() + edges_hash = hash_dict(self.edges) + centers_hash = hash_dict(self.centers) + corners_hash = hash_dict(self.corners) + return int( + hashlib.md5( + (edges_hash + centers_hash + corners_hash).encode() + ).hexdigest(), + 16, + ) + + def __repr__(self): + return f"{self.edges=}, {self.centers=}, {self.corners=}" + + def M(self): + cyclic_shift(self.edges, M_SLICE_EDGES, 3) + for edge in M_SLICE_EDGES: + self.edges[edge].oriented = not self.edges[edge].oriented + cyclic_shift(self.centers, CENTERS, 3) + + def M2(self): + cyclic_shift(self.edges, M_SLICE_EDGES, 2) + cyclic_shift(self.centers, CENTERS, 2) + + def Mp(self): + cyclic_shift(self.edges, M_SLICE_EDGES, 1) + for edge in M_SLICE_EDGES: + self.edges[edge].oriented = not self.edges[edge].oriented + cyclic_shift(self.centers, CENTERS, 1) + + def U(self): + cyclic_shift(self.edges, U_EDGES, 3) + cyclic_shift(self.corners, U_CORNERS, 3) + + def U2(self): + cyclic_shift(self.edges, U_EDGES, 2) + cyclic_shift(self.corners, U_CORNERS, 2) + + def Up(self): + cyclic_shift(self.edges, U_EDGES, 1) + cyclic_shift(self.corners, U_CORNERS, 1) + + def cycle_corners(self, n: int): + cyclic_shift(self.corners, U_CORNERS, 4 - n) + + def fix_auf(self) -> str: + match self.corners["UFR"]: + case "UFR": + return "" + case "UBR": + self.Up() + return "U'" + case "UBL": + self.U2() + return "U2" + case "UFL": + self.U() + return "U" + return "" + + def alg(self, alg: str): + move_functions = { + "U": self.U, + "U2": self.U2, + "U2'": self.U2, + "U'": self.Up, + "M": self.M, + "M2": self.M2, + "M2'": self.M2, + "M'": self.Mp, + } + for move in alg.split(): + move_functions[move]() + + +def reverse_algorithm(moves: List[str]) -> List[str]: + def invert_move(move): + if move.endswith("'"): + return move[:-1] + elif move.endswith("2"): + return move + else: + return move + "'" + + reversed_moves = [invert_move(move) for move in reversed(moves)] + return reversed_moves + + +def condense_algorithm(moves: List[str]) -> List[str]: + def apply_reduction(moves: List[str]) -> List[str]: + result = [] + for move in moves: + base_move = move[0] + multiplier = 1 + if move.endswith("2"): + multiplier = 2 + if move.endswith("'"): + multiplier = 3 + result.extend([base_move] * multiplier) + return result + + def reduce_moves(moves: List[str]) -> List[Tuple[str, int]]: + if not moves: + return [] + + condensed = [] + current_element = moves[0] + count = 1 + + for element in moves[1:]: + if element == current_element: + count += 1 + else: + condensed.append((current_element, count)) + current_element = element + count = 1 + + # Append the last element group + condensed.append((current_element, count)) + + return condensed + + def build_reduced_alg(moves: List[Tuple[str, int]]) -> List[str]: + result = [] + for move in moves: + move_type, count = move + count = count % 4 + match count: + case 1: + result.append(move_type) + case 2: + result.append(move_type + "2") + case 3: + result.append(move_type + "'") + return result + + reduced_moves = apply_reduction(moves) + # print(reduced_moves) + counted_moves = reduce_moves(reduced_moves) + # print(counted_moves) + reduced_alg = build_reduced_alg(counted_moves) + # print(reduced_alg) + return reduced_alg diff --git a/eolr.py b/eolr.py new file mode 100644 index 0000000..a57d85e --- /dev/null +++ b/eolr.py @@ -0,0 +1,126 @@ +from dataclasses import dataclass +from enum import Enum +import itertools +from typing import List, Tuple, Iterator +from cube import Edge, LSECube + + +class EOLROrientation(Enum): + Arrow = ["UR", "UF", "UL", "DF"] + BackArrow = ["UR", "UF", "UL", "DB"] + FourZero = ["UB", "UR", "UF", "UL"] + OneOne = ["UB", "DF"] + BackOneOne = ["UB", "DB"] + SixFlip = ["UB", "UR", "UF", "UL", "DF", "DB"] + Solved = [] + TwoAdjTwo = ["UB", "UR", "DF", "DB"] + TwoAdjZero = ["UB", "UR"] + TwoOppTwo = ["UB", "UF", "DF", "DB"] + TwoOppZero = ["UB", "UF"] + ZeroTwo = ["DF", "DB"] + + +@dataclass +class EOLRPermutation: + UR: str + UL: str + + +EOLR_PERMUTATIONS = [ + EOLRPermutation(ur_pos, ul_pos) + for ul_pos, ur_pos in itertools.permutations( + ["UB", "UR", "UF", "UL", "DF", "DB"], 2 + ) +] + + +EOLR_EDGES = [ + "UB", + "UR", + "UF", + "UL", + "DF", + "DB", +] + + +def apply_eolr_orientation(cube: LSECube, ori: EOLROrientation): + for edge in ori.value: + cube.edges[edge].oriented = not cube.edges[edge].oriented + + +def apply_eolr_permutation(cube: LSECube, perm: EOLRPermutation): + cube.edges[perm.UR], cube.edges["UR"] = cube.edges["UR"], cube.edges[perm.UR] + cube.edges[perm.UL], cube.edges["UL"] = cube.edges["UL"], cube.edges[perm.UL] + + +def apply_eolr_auf(cube: LSECube, auf: int): + cube.cycle_corners(auf) + + +def eolrb_states_generator( + orientations: List[EOLROrientation], permutations: List[EOLRPermutation] +) -> Tuple[Iterator[Tuple[LSECube, EOLROrientation, EOLRPermutation, int]], int]: + num_permutations = len(orientations) * len(permutations) * 4 + + def generator(): + for ori in orientations: + for perm in permutations: + for auf in range(4): + state = LSECube() + apply_eolr_auf(state, auf) + apply_eolr_orientation(state, ori) + apply_eolr_permutation(state, perm) + yield (state, ori, perm, auf) + + return generator(), num_permutations + + +def lr_solved(cube: LSECube): + return cube.edges["UR"] == Edge("UR", True) and cube.edges["UL"] == Edge("UL", True) + + +def eo_solved_non_mc(cube: LSECube) -> bool: + return ( + cube.edges["UB"].oriented + and cube.edges["UF"].oriented + and cube.edges["DF"].oriented + and cube.edges["DB"].oriented + ) + + +def centers_solved_non_mc(cube: LSECube) -> bool: + return cube.centers["U"] in ["U", "D"] + + +def eo_solved_mc(cube: LSECube) -> bool: + return ( + not cube.edges["UB"].oriented + and not cube.edges["UF"].oriented + and not cube.edges["DF"].oriented + and not cube.edges["DB"].oriented + ) + + +def centers_solved_mc(cube: LSECube) -> bool: + return cube.centers["U"] in ["F", "B"] + + +def eolrb_solved(cube: LSECube) -> bool: + """ + this function has weird side effects and modifies cube but we don't care about it for now + """ + if not lr_solved(cube): + return False + if centers_solved_non_mc(cube) and eo_solved_non_mc(cube): + return True + if centers_solved_mc(cube) and eo_solved_mc(cube): + return True + return False + + +def eolrb_solution_mc(cube: LSECube) -> bool: + """ + this function only works if eolrb is solved + """ + return centers_solved_mc(cube) and eo_solved_mc(cube) diff --git a/generate_regrips.py b/generate_regrips.py new file mode 100644 index 0000000..8e2d2a7 --- /dev/null +++ b/generate_regrips.py @@ -0,0 +1,24 @@ +import itertools +import yaml + + +def generate_regrips(): + finger_grips = { + "index": ["B", "BL", "FL", "F"], + "ring": ["B", "BD", "FD"], + "pinky": ["B", "BD", "FD", "F", "F floating"], + } + data = [] + for finger, grips in finger_grips.items(): + for pre, post in itertools.permutations(grips, 2): + data.append( + { + "finger": finger, + "pre": pre, + "post": post, + "score": 1.0, + } + ) + print(yaml.safe_dump(data, sort_keys=False)) + +generate_regrips() diff --git a/moves.yaml b/moves.yaml new file mode 100644 index 0000000..a23eaae --- /dev/null +++ b/moves.yaml @@ -0,0 +1,241 @@ +finger_tricks: + - name: "U push" + move: "U" + grip_pre: + index: BL + grip_post: + index: B + score: 5 + - name: "U flick" + move: "U" + grip_pre: + index: F + grip_post: + index: BL + score: 4 + - name: "U' push" + move: "U'" + grip_pre: + index: FL + grip_post: + index: F + score: 5 + - name: "U' flick" + move: "U'" + grip_pre: + index: B + grip_post: + index: FL + score: 3 + - name: "U2 feido" + move: "U2" + grip_pre: + index: B + grip_post: + index: F + score: 5 + - name: "U2 double flick" + move: "U2" + grip_pre: + index: B + grip_post: + index: FL + score: 6 + - name: "U2' double flick" + move: "U2" + grip_pre: + index: F + grip_post: + index: BL + score: 9 + - name: "U2 beido" + move: "U2" + grip_pre: + index: F + grip_post: + index: B + score: 5 + - name: "M' ring flick" + move: "M'" + grip_pre: + ring: B + grip_post: + ring: DF + score: 3 + - name: "M' pinky flick" + move: "M'" + grip_pre: + pinky: B + grip_post: + pinky: F floating + score: 6 + - name: "M push" + move: "M" + grip_pre: + pinky: F + grip_post: + pinky: DB + score: 6 + - name: "M2 double flick" + move: "M2" + grip_pre: + pinky: B + ring: B + grip_post: + pinky: F floating + ring: DB + score: 7 +regrips: + # TODO: rethink this format? + - finger: index + pre: B + post: BL + score: 1 + - finger: index + pre: B + post: FL + score: 1 + - finger: index + pre: B + post: F + score: 1 + - finger: index + pre: BL + post: B + score: 1 + - finger: index + pre: BL + post: FL + score: 1 + - finger: index + pre: BL + post: F + score: 1 + - finger: index + pre: FL + post: B + score: 1 + - finger: index + pre: FL + post: BL + score: 1 + - finger: index + pre: FL + post: F + score: 1 + - finger: index + pre: F + post: B + score: 1 + - finger: index + pre: F + post: BL + score: 1 + - finger: index + pre: F + post: FL + score: 1 + - finger: ring + pre: B + post: DB + score: 1 + - finger: ring + pre: B + post: DF + score: 1 + - finger: ring + pre: DB + post: B + score: 1 + - finger: ring + pre: DB + post: DF + score: 1 + - finger: ring + pre: DF + post: B + score: 1 + - finger: ring + pre: DF + post: DB + score: 1 + - finger: pinky + pre: B + post: DB + score: 1 + - finger: pinky + pre: B + post: DF + score: 1 + - finger: pinky + pre: B + post: F + score: 1 + - finger: pinky + pre: B + post: F floating + score: 1 + - finger: pinky + pre: DB + post: B + score: 1 + - finger: pinky + pre: DB + post: DF + score: 1 + - finger: pinky + pre: DB + post: F + score: 1 + - finger: pinky + pre: DB + post: F floating + score: 1 + - finger: pinky + pre: DF + post: B + score: 1 + - finger: pinky + pre: DF + post: DB + score: 1 + - finger: pinky + pre: DF + post: F + score: 1 + - finger: pinky + pre: DF + post: F floating + score: 1 + - finger: pinky + pre: F + post: B + score: 1 + - finger: pinky + pre: F + post: DB + score: 1 + - finger: pinky + pre: F + post: DF + score: 1 + - finger: pinky + pre: F + post: F floating + score: 1 + - finger: pinky + pre: F floating + post: B + score: 1 + - finger: pinky + pre: F floating + post: DB + score: 1 + - finger: pinky + pre: F floating + post: DF + score: 1 + - finger: pinky + pre: F floating + post: F + score: 1 diff --git a/scorer.py b/scorer.py new file mode 100644 index 0000000..91b4cde --- /dev/null +++ b/scorer.py @@ -0,0 +1,129 @@ +from typing import Dict, List +import yaml +import dacite +from dataclasses import dataclass + + +@dataclass +class FingerTrick: + name: str + move: str + grip_pre: Dict[str, str] + grip_post: Dict[str, str] + score: float + + +@dataclass +class Regrip: + finger: str + pre: str + post: str + score: float + + +@dataclass +class Config: + finger_tricks: List[FingerTrick] + regrips: List[Regrip] + + +def home_grip() -> Dict[str, str]: + return { + "index": "B", # this could also be BL + "ring": "B", + "pinky": "BD", + } + + +def load_config(file_path: str) -> Config: + with open(file_path, "r") as f: + data_dict = yaml.safe_load(f.read()) + return dacite.from_dict(data_class=Config, data=data_dict) + + +def grip_correct(current_grip: Dict[str, str], required_grip: Dict[str, str]): + return all(item in current_grip.items() for item in required_grip.items()) + + +@dataclass +class FingerTrickWithRegrip: + finger_trick: FingerTrick + regrips: List[Regrip] + + def score(self) -> float: + score = 0.0 + score += self.finger_trick.score + if self.regrips: + longest_regrip = max(self.regrips, key=lambda regrip: regrip.score) + score += longest_regrip.score + return score + + +def calculate_finger_trick_regrips( + config: Config, finger_trick: FingerTrick, grip: Dict[str, str] +) -> List[Regrip]: + regrips = [] + for finger in finger_trick.grip_pre.keys(): + current_location = grip[finger] + desired_location = finger_trick.grip_pre.get(finger) + if current_location != desired_location: + regrip = next( + ( + regrip + for regrip in config.regrips + if regrip.finger == finger + and regrip.pre == current_location + and regrip.post == desired_location + ), + None, + ) + if regrip: + regrips.append(regrip) + return regrips + + +def generate_finger_tricks( + config: Config, moves: List[str] +) -> List[FingerTrickWithRegrip]: + grip = home_grip() + alg = [] + for move in moves: + # print("current grip:", grip) + # prit("current move:", move) + possible_finger_tricks = [ + finger_trick + for finger_trick in config.finger_tricks + if move == finger_trick.move + ] + # print("possible finger tricks:", possible_finger_tricks) + finger_tricks_with_regrips = [ + FingerTrickWithRegrip( + finger_trick=finger_trick, + regrips=calculate_finger_trick_regrips(config, finger_trick, grip), + ) + for finger_trick in possible_finger_tricks + ] + best_finger_trick = min( + finger_tricks_with_regrips, key=lambda item: item.score() + ) + # print("best finger trick:", best_finger_trick) + # apply regrips + for regrip in best_finger_trick.regrips: + grip[regrip.finger] = regrip.post + # apply move + grip.update(best_finger_trick.finger_trick.grip_post) + alg.append(best_finger_trick) + return alg + + +def build_pretty_string_from_finger_tricks_with_regrips( + finger_tricks_with_regrips: List[FingerTrickWithRegrip], +) -> List[str]: + elems = [] + + for finger_trick_with_regrips in finger_tricks_with_regrips: + for regrip in finger_trick_with_regrips.regrips: + elems.append(f"regrip {regrip.finger} from {regrip.pre} to {regrip.post}") + elems.append(finger_trick_with_regrips.finger_trick.name) + + return elems diff --git a/solver.py b/solver.py new file mode 100644 index 0000000..c37a31c --- /dev/null +++ b/solver.py @@ -0,0 +1,104 @@ +from collections import defaultdict +import itertools +import copy + +from dataclasses import dataclass +from cube import LSECube, condense_algorithm, reverse_algorithm +from eolr import eolrb_solved +from typing import Dict, List, Tuple + + +@dataclass +class EOLRBSolution: + alg: List[str] + pre_auf: str + post_auf: str + mc: bool + + +def lse_brute_force_generator(max_length: int): + list1 = ["M", "M'", "M2"] + list2 = ["U", "U'", "U2"] + + def valid_combination(comb): + # Ensure no adjacent elements are from the same list + if any( + (comb[i] in list1 and comb[i + 1] in list1) + or (comb[i] in list2 and comb[i + 1] in list2) + for i in range(len(comb) - 1) + ): + return False + + # # Ensure the combination does not end with a U move + # if comb and comb[-1] in list2: + # return False + + return True + + def generate_combinations(): + # Generate combinations of increasing lengths + for length in range(1, max_length + 1): + for comb in itertools.product(list1 + list2, repeat=length): + if valid_combination(comb): + yield list(comb) + + return generate_combinations() + + +# +# prune table +# + + +def create_prune_table(prune_depth: int): + prune_table = defaultdict(list) + cube = LSECube() + prune_table[hash(cube)] = [[]] + + generator = lse_brute_force_generator(prune_depth) + + for alg in generator: + # TODO: turn the prune table into a dict of list values to store multiple solutions + cube.reset() + cube.alg(" ".join(alg)) + setup = reverse_algorithm(alg) + prune_table[hash(cube)].append(setup) + + return prune_table + + +def extract_pre_auf(alg: List[str]) -> Tuple[str, List[str]]: + if alg[0].startswith("U"): + pre_auf = alg.pop(0) + return pre_auf, alg + else: + return "", alg + + +def solve_eolrb(cube: LSECube, prune_table: Dict, solve: int): + for moves in lse_brute_force_generator(solve): + c = copy.deepcopy(cube) + c.alg(" ".join(moves)) + if hash(c) in prune_table: + for alg_start in prune_table[hash(c)]: + alg = condense_algorithm(alg_start + moves) + # TODO: this is very weird + for _ in range(10): + alg = condense_algorithm(alg) + if len(alg) == 0: + yield EOLRBSolution(alg, "", "", False) + else: + # TODO: check if this is mc + pre_auf = "" + if alg[0].startswith("U"): + pre_auf = alg.pop(0) + post_auf = "" + if alg[-1].startswith("U"): + post_auf = alg.pop(-1) + yield EOLRBSolution(alg, pre_auf, post_auf, False) + # for auf in ["U", "U2", "U'", ""]: + # c = copy.deepcopy(cube) + # auf_alg = f"{' '.join(alg)} {auf}" + # c.alg(auf_alg) + # if eolrb_solved(c): + # yield alg