From a414cb35653aa422a69da08852c3dd1ee32e98c0 Mon Sep 17 00:00:00 2001 From: Abraham Hsu Date: Mon, 2 Mar 2026 19:23:42 -0800 Subject: [PATCH 01/29] Changes to hash and solver, adds db directory --- database/db/.gitignore | 4 ++ solver/src/solver/solver.py | 135 ++++++++++++++++-------------------- 2 files changed, 64 insertions(+), 75 deletions(-) create mode 100644 database/db/.gitignore diff --git a/database/db/.gitignore b/database/db/.gitignore new file mode 100644 index 0000000..c5f1541 --- /dev/null +++ b/database/db/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file itself +!.gitignore \ No newline at end of file diff --git a/solver/src/solver/solver.py b/solver/src/solver/solver.py index 4e3671f..14e75f5 100644 --- a/solver/src/solver/solver.py +++ b/solver/src/solver/solver.py @@ -1,4 +1,4 @@ -from collections import deque +from collections import deque, defaultdict from models import * from database import DuckDB, SqliteDB import time @@ -10,7 +10,7 @@ class Solver: def __init__(self, game: Game): self._game = game self.solution = {} - self.parent_map = {} + self.parent_map = defaultdict(list) self.unsolved_children = {} def solve(self, overwrite=False, variant=None): @@ -22,7 +22,7 @@ def solve(self, overwrite=False, variant=None): for variant in variants: self.solution = {} - self.parent_map = {} + self.parent_map = defaultdict(list) self.unsolved_children = {} self.game = self._game(variant) self.db = SqliteDB(self._game.id, variant, ro=False) @@ -49,99 +49,92 @@ def solve(self, overwrite=False, variant=None): else: print(f"{self.game.id}, variant: {variant} already solved.") - def get_children(self, position): - unhashed = self.game.unhash_ext(position) - moves = self.game.generate_moves(unhashed) - children = list(map(lambda m: self.game.hash_ext(self.game.do_move(unhashed, m)), moves)) + def get_children(self, position, generate_moves, do_move): + moves = generate_moves(position) + children = list(map(lambda m: do_move(position, m), moves)) return children def discover(self): visited = set() q = deque() - start = self.game.hash_ext(self.game.start()) + start = self.game.start() + + hash_ext = self.game.hash_ext + primitive = self.game.primitive + do_move = self.game.do_move + gen_moves = self.game.generate_moves + n_players = self.game.n_players + q.appendleft(start) - visited.add(start) + visited.add(hash_ext(start)) while q: - position = q.pop() - value = self.game.primitive(self.game.unhash_ext(position)) + position = q.popleft() + hashed_position = hash_ext(position) + value = primitive(position) if value is not None: - self.solution[position] = (REMOTENESS_TERMINAL, value) - self.unsolved_children[position] = 0 + self.solution[hashed_position] = (REMOTENESS_TERMINAL, value) + self.unsolved_children[hashed_position] = 0 else: - children = self.get_children(position) - self.unsolved_children[position] = len(children) - if not children and self.game.n_players == 1: - self.solution[position] = (REMOTENESS_TERMINAL, Value.Loss) + children = self.get_children(position, gen_moves, do_move) + unique_children = {hash_ext(c): c for c in children} + self.unsolved_children[hashed_position] = len(unique_children) + if not unique_children and n_players == 1: + self.solution[hashed_position] = (REMOTENESS_TERMINAL, Value.Loss) + + for hashed_child, child in unique_children.items(): + self.parent_map[hashed_child].append(hashed_position) + + if hashed_child not in visited: + visited.add(hashed_child) + q.append(child) + - for child in children: - if not self.parent_map.get(child): - self.parent_map[child] = set() - self.parent_map[child].add(position) - if child not in visited: - visited.add(child) - q.appendleft(child) - def propagate(self): wins = deque() ties = deque() losses = deque() + for pos, (_, val) in self.solution.items(): match val: - case Value.Win: wins.appendleft(pos) - case Value.Tie: ties.appendleft(pos) - case Value.Loss: losses.appendleft(pos) + case Value.Win: wins.append(pos) + case Value.Tie: ties.append(pos) + case Value.Loss: losses.append(pos) while wins or ties or losses: - position = None + hashed_position = None if losses: - position = losses.pop() + hashed_position = losses.popleft() elif wins: - position = wins.pop() + hashed_position = wins.popleft() else: - position = ties.pop() - (curr_rem, curr_val) = self.solution.get(position) + hashed_position = ties.popleft() + (curr_rem, curr_val) = self.solution[hashed_position] parent_rem = curr_rem + 1 parent_val: Value = self.parent_value(curr_val) - parents = self.parent_map.get(position, set()) - for parent in parents: - unsolved_children = self.unsolved_children[parent] + parents = self.parent_map.get(hashed_position, []) + for hashed_parent in parents: + unsolved_children = self.unsolved_children[hashed_parent] if unsolved_children == 0: continue if parent_val == Value.Loss: - self.unsolved_children[parent] = unsolved_children - 1 + self.unsolved_children[hashed_parent] -= 1 else: - self.unsolved_children[parent] = 0 - ex_parent_sol = self.solution.get(parent) - if ex_parent_sol is None: - self.solution[parent] = (parent_rem, parent_val) - else: - (ex_parent_rem, ex_parent_val) = ex_parent_sol - propagate = False - if ex_parent_val < parent_val: - propagate = True - elif ex_parent_val == parent_val: - if ex_parent_val == Value.Loss: - if ex_parent_rem < parent_rem: - propagate = True - elif ex_parent_rem > parent_rem: - propagate = True - if propagate: - self.solution[parent] = (parent_rem, parent_val) - if self.unsolved_children[parent] == 0: - (_, p_val) = self.solution[parent] - match p_val: - case Value.Win: wins.appendleft(parent) - case Value.Tie: ties.appendleft(parent) - case Value.Loss: losses.appendleft(parent) - + self.unsolved_children[hashed_parent] = 0 + + if self.unsolved_children[hashed_parent] == 0: + self.solution[hashed_parent] = (parent_rem, parent_val) + match parent_val: + case Value.Win: wins.append(hashed_parent) + case Value.Tie: ties.append(hashed_parent) + case Value.Loss: losses.append(hashed_parent) def resolve_draws(self): - for pos in self.unsolved_children.keys(): + for pos_hash in self.unsolved_children.keys(): if self.game.n_players == 2: - if self.unsolved_children[pos] > 0: - self.solution[pos] = (REMOTENESS_DRAW, Value.Draw) + if self.unsolved_children[pos_hash] > 0: + self.solution[pos_hash] = (REMOTENESS_DRAW, Value.Draw) else: - if pos not in self.solution or self.unsolved_children[pos] > 0: - self.solution[pos] = (REMOTENESS_DRAW, Value.Loss) + if pos_hash not in self.solution or self.unsolved_children[pos_hash] > 0: + self.solution[pos_hash] = (REMOTENESS_DRAW, Value.Loss) def parent_value(self, val: Value) -> Value: @@ -154,15 +147,7 @@ def parent_value(self, val: Value) -> Value: return Value.Win else: return val - - def print(self): - if self.solution: - sol = [(position, rem, value) for position, (rem, value) in self.solution.items()] - else: - sol = self.db.get_all() - for (position, rem, value) in sol: - print(f'state: {self.game.to_string(position, StringMode.Readable)} | remoteness: {rem} | value: {Value(value).name}') - + def get_remoteness(self, state: int) -> int: rem, _ = self.db.get(state) if rem is None: From 4565aea23e2ead895ab80cb2eb1e4f6ad6df6f92 Mon Sep 17 00:00:00 2001 From: Abraham Hsu Date: Mon, 2 Mar 2026 19:32:48 -0800 Subject: [PATCH 02/29] Removed unecessary get_remoteness function --- solver/src/solver/solver.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/solver/src/solver/solver.py b/solver/src/solver/solver.py index 14e75f5..c67af54 100644 --- a/solver/src/solver/solver.py +++ b/solver/src/solver/solver.py @@ -147,9 +147,3 @@ def parent_value(self, val: Value) -> Value: return Value.Win else: return val - - def get_remoteness(self, state: int) -> int: - rem, _ = self.db.get(state) - if rem is None: - return -1 - return rem From d75a88c4803b508636702a8311a08859d6590469 Mon Sep 17 00:00:00 2001 From: Abraham Hsu Date: Mon, 2 Mar 2026 20:00:24 -0800 Subject: [PATCH 03/29] Updated README --- games/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/games/README.md b/games/README.md index 7626f84..8275ce4 100644 --- a/games/README.md +++ b/games/README.md @@ -13,9 +13,9 @@ Required functions: * `to_string` : transform and return the given integer position into a string representation, based on the given mode. * `from_string` : transform and return the given `StringMode.Readable` position string into the integer representation. * `move_to_string` : transform and return the given integer move into a string representation, based on the given mode. +* `hash_ext` : given a position, return a number representing the hash number of the position. If you decide not to implement this function, the default behavior is returning position. Therefore, position will have to be an integer. Recommended functions: -* `hash` : used to transform an internal, readable representation of a position into an integer in a reversible way. You can design this as you see fit. May be unnecessary for games/puzzles that have an obvious integer state representation (i.e. ten-to-zero). -* `unhash` : reverse of hash. +* `unhash_ext` : reverse of hash_ext. The default behavior is to return hashed position, which, by default, is just an integer. Refer to hash_ext for details on default behavior. Run `uv run solver ` to solve all variants, or add `-v ` for specific variants. To overwrite an existing database and resolve, add `-o`. From 2c7895840778dd47f9419e22c10236d1d8a1e7f9 Mon Sep 17 00:00:00 2001 From: michael-wsp Date: Mon, 2 Mar 2026 20:55:56 -0800 Subject: [PATCH 04/29] reverted pancakes hashing, fixed solver state exploration where multiple states map to one hash --- games/src/games/pancakes.py | 17 ++++++++--------- solver/src/solver/solver.py | 20 +++++++++++++------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/games/src/games/pancakes.py b/games/src/games/pancakes.py index e76f98e..68a9740 100644 --- a/games/src/games/pancakes.py +++ b/games/src/games/pancakes.py @@ -27,7 +27,7 @@ def start(self) -> int: direction_choices = [0, 1] random.shuffle(widths) directions = [random.choice(direction_choices) for _ in range(self.height)] - return (widths, directions) + return self.hash(widths, directions) def generate_moves(self, position: int) -> list[int]: """ @@ -39,18 +39,18 @@ def do_move(self, position, move: int) -> int: """ Returns the resulting position of applying move to position. """ - (widths, dirs) = position + (widths, dirs) = self.unhash(position) new_widths = widths[:] new_dirs = dirs[:] new_widths[move:] = reversed(widths[move:]) new_dirs[move:] = [x ^ 0b1 for x in reversed(dirs[move:])] - return (new_widths, new_dirs) + return self.hash(new_widths, new_dirs) def primitive(self, position) -> Optional[Value]: """ Returns a Value enum which defines whether the current position is a win, loss, or non-terminal. """ - (widths, dirs) = position + (widths, dirs) = self.unhash(position) correct_order = widths == sorted(widths, reverse=True) correct_direction = all(x == 0 for x in dirs) if correct_order and correct_direction: @@ -63,7 +63,7 @@ def to_string(self, position, mode: StringMode) -> str: """ pos_arr = [] adder = ord('a') - 1 - (widths, dirs) = position + (widths, dirs) = self.unhash(position) for i in range(self.height): val = str(widths[i]) if dirs[i] == 0 else chr(widths[i] + adder) pos_arr.append(val) @@ -87,7 +87,7 @@ def from_string(self, strposition: str) -> int: else: dirs.append(0) widths.append(int(char)) - return (widths, dirs) + return self.hash(widths, dirs) @@ -97,8 +97,7 @@ def move_to_string(self, move: int, mode: StringMode) -> str: """ return f'A_-_{move}_x' - def hash_ext(self, position) -> int: - (widths, directions) = position + def hash(self, widths, directions) -> int: h = 0 n = self.height for i in range(n): @@ -111,7 +110,7 @@ def hash_ext(self, position) -> int: h = (h << n) | direction_hash return h - def unhash_ext(self, position) -> tuple[list[int]]: + def unhash(self, position) -> tuple[list[int]]: width_arr = [] direction_arr = [] n = self.height diff --git a/solver/src/solver/solver.py b/solver/src/solver/solver.py index c67af54..89654be 100644 --- a/solver/src/solver/solver.py +++ b/solver/src/solver/solver.py @@ -66,7 +66,7 @@ def discover(self): n_players = self.game.n_players q.appendleft(start) - visited.add(hash_ext(start)) + visited.add(start) while q: position = q.popleft() hashed_position = hash_ext(position) @@ -76,18 +76,24 @@ def discover(self): self.unsolved_children[hashed_position] = 0 else: children = self.get_children(position, gen_moves, do_move) + unique_children = {hash_ext(c): c for c in children} - self.unsolved_children[hashed_position] = len(unique_children) + if self.unsolved_children.get(hashed_position, None) is None: + self.unsolved_children[hashed_position] = 0 + if not unique_children and n_players == 1: self.solution[hashed_position] = (REMOTENESS_TERMINAL, Value.Loss) - for hashed_child, child in unique_children.items(): - self.parent_map[hashed_child].append(hashed_position) - - if hashed_child not in visited: - visited.add(hashed_child) + for child in children: + if child not in visited: + visited.add(child) q.append(child) + for hashed_child, child in unique_children.items(): + if hashed_child != hashed_position: + self.parent_map[hashed_child].append(hashed_position) + self.unsolved_children[hashed_position] += 1 + def propagate(self): wins = deque() From 36399ebf2ee2dfd408e48fd65a1e79104f24fdf4 Mon Sep 17 00:00:00 2001 From: michael-wsp Date: Mon, 2 Mar 2026 21:54:52 -0800 Subject: [PATCH 05/29] Fixed state explosion issue --- solver/src/solver/solver.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/solver/src/solver/solver.py b/solver/src/solver/solver.py index 89654be..f232f20 100644 --- a/solver/src/solver/solver.py +++ b/solver/src/solver/solver.py @@ -59,6 +59,7 @@ def discover(self): q = deque() start = self.game.start() + no_hash = hasattr(self.game, "no_hash") hash_ext = self.game.hash_ext primitive = self.game.primitive do_move = self.game.do_move @@ -66,7 +67,10 @@ def discover(self): n_players = self.game.n_players q.appendleft(start) - visited.add(start) + if no_hash is None: + visited.add(hash_ext(start)) + else: + visited.add(start) while q: position = q.popleft() hashed_position = hash_ext(position) @@ -75,24 +79,20 @@ def discover(self): self.solution[hashed_position] = (REMOTENESS_TERMINAL, value) self.unsolved_children[hashed_position] = 0 else: - children = self.get_children(position, gen_moves, do_move) - - unique_children = {hash_ext(c): c for c in children} - if self.unsolved_children.get(hashed_position, None) is None: - self.unsolved_children[hashed_position] = 0 - - if not unique_children and n_players == 1: + children = self.get_children(position, gen_moves, do_move) + if not children and n_players == 1: self.solution[hashed_position] = (REMOTENESS_TERMINAL, Value.Loss) for child in children: - if child not in visited: - visited.add(child) - q.append(child) - - for hashed_child, child in unique_children.items(): + hashed_child = hash_ext(child) + child_alt = hashed_child if no_hash is None else child if hashed_child != hashed_position: self.parent_map[hashed_child].append(hashed_position) - self.unsolved_children[hashed_position] += 1 + prev_unsolved = self.unsolved_children.get(hashed_position, 0) + self.unsolved_children[hashed_position] = prev_unsolved + 1 + if child_alt not in visited: + visited.add(child_alt) + q.append(child) def propagate(self): From 97c02c400a6f0880ed93edc9ee9062620961e195 Mon Sep 17 00:00:00 2001 From: michael-wsp Date: Mon, 2 Mar 2026 21:57:30 -0800 Subject: [PATCH 06/29] Fixed reverse behavior for no_hash --- solver/src/solver/solver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/solver/src/solver/solver.py b/solver/src/solver/solver.py index f232f20..969ad9d 100644 --- a/solver/src/solver/solver.py +++ b/solver/src/solver/solver.py @@ -67,10 +67,10 @@ def discover(self): n_players = self.game.n_players q.appendleft(start) - if no_hash is None: - visited.add(hash_ext(start)) - else: + if no_hash: visited.add(start) + else: + visited.add(hash_ext(start)) while q: position = q.popleft() hashed_position = hash_ext(position) @@ -85,7 +85,7 @@ def discover(self): for child in children: hashed_child = hash_ext(child) - child_alt = hashed_child if no_hash is None else child + child_alt = child if no_hash else hashed_child if hashed_child != hashed_position: self.parent_map[hashed_child].append(hashed_position) prev_unsolved = self.unsolved_children.get(hashed_position, 0) From 032f923b9f787002a22a697326466a5af314d63b Mon Sep 17 00:00:00 2001 From: Sora Wongsonegoro Date: Tue, 3 Mar 2026 15:00:01 -0800 Subject: [PATCH 07/29] added stormy seas to gamemanager --- games/src/games/StormySeas | 1 + 1 file changed, 1 insertion(+) create mode 160000 games/src/games/StormySeas diff --git a/games/src/games/StormySeas b/games/src/games/StormySeas new file mode 160000 index 0000000..713025f --- /dev/null +++ b/games/src/games/StormySeas @@ -0,0 +1 @@ +Subproject commit 713025ffd5001922ca99b4e1dc60ea8ea36eb89c From d1243f9f3eeab6cf5c20cbc69aca5b408f0c63f6 Mon Sep 17 00:00:00 2001 From: Sora Wongsonegoro Date: Tue, 3 Mar 2026 17:41:06 -0800 Subject: [PATCH 08/29] tried to reduce integer reps --- games/src/games/StormySeas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games/src/games/StormySeas b/games/src/games/StormySeas index 713025f..2290774 160000 --- a/games/src/games/StormySeas +++ b/games/src/games/StormySeas @@ -1 +1 @@ -Subproject commit 713025ffd5001922ca99b4e1dc60ea8ea36eb89c +Subproject commit 2290774dd426fd1677ed1777d42f7db14a442e8f From 6308826f5f77cf414dceb93c6ef24140ff46430d Mon Sep 17 00:00:00 2001 From: Sora Wongsonegoro Date: Tue, 3 Mar 2026 21:13:31 -0800 Subject: [PATCH 09/29] added stormy seas --- games/src/games/StormySeas | 2 +- games/src/games/StormySeasV3.py | 378 ++++++++++++++++++++++++++++++++ games/src/games/game_manager.py | 2 + 3 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 games/src/games/StormySeasV3.py diff --git a/games/src/games/StormySeas b/games/src/games/StormySeas index 2290774..7878965 160000 --- a/games/src/games/StormySeas +++ b/games/src/games/StormySeas @@ -1 +1 @@ -Subproject commit 2290774dd426fd1677ed1777d42f7db14a442e8f +Subproject commit 78789657f820e91b34ffa83c7beba4b9800bc224 diff --git a/games/src/games/StormySeasV3.py b/games/src/games/StormySeasV3.py new file mode 100644 index 0000000..cb4aefc --- /dev/null +++ b/games/src/games/StormySeasV3.py @@ -0,0 +1,378 @@ +from models import Game, Value, StringMode +from typing import Optional + +class StormySeas(Game): + id = 'stormyseas' + variants = ["a", "b", "c"] + n_players = 1 + cyclic = False + colors = ["R", "G", "O"] + + def __init__(self, variant_id: str): + """ + Define instance variables here (i.e. variant information) + """ + if variant_id not in StormySeas.variants: + raise ValueError("Variant not defined") + self._variant_id = variant_id + board_rows = [] + + def start(self) -> int: + if self._variant_id == "a": + self.board_rows = ["101011100","101110100","101101100","101011100","101011100","101110100","110110100","111011100"] + + # use ternary digits to represent shifts? + curr_shift_string = "11000020" + boat_pos = "123256227142" # first two digits are location, third digit is direction (1 = up, 2 = right, 3 = down, 4 = left), last digit is length + hash = self.hash(curr_shift_string + boat_pos) + return hash + elif self._variant_id == "b": + # Add variant b starting position + string_rep = "010101111001011101010110110010101110010101110001011101000011011011110111001123238221142" + return self.hash(string_rep) + elif self._variant_id == "c": + # Add variant c starting position + string_rep = "010101111001011101010110110010101110010101110001011101000011011011110111001123238221142" + return self.hash(string_rep) + + return 0 + + def generate_moves(self, position: int): + """ + Returns a list of positions given the input position. + """ + string_rep = self.translate(self.unhash(position)) + list_to_return = [] + + string_rep_list = list(string_rep) + + #DO NOT MODIFY THESE POSITIONS DIRECTLY + board_list = string_rep_list[:72] + boat_1 = string_rep_list[72:76] + boat_2 = string_rep_list[76:80] + boat_3 = string_rep_list[80:84] + boat_list = [boat_1, boat_2, boat_3] + + #up + for boat in boat_list: + current_pos = int(boat[0]) * 10 + int(boat[1]) + current_dir = int(boat[2]) + + #up + if current_dir == 1: + if current_pos <= 9: + continue + elif board_list[current_pos - 9 - 1] == '0': + new_board = board_list[:current_pos - 9 - 1] + ['1'] + board_list[current_pos - 9:current_pos - 1] + ['0'] + board_list[current_pos:] + new_boat = [str(current_pos - 9)] + boat[1:] + list_to_return.append(self.hash(self.untranslate((''.join(new_board) + ''.join(new_boat))))) + #down + elif current_dir == 3: + if current_pos >= 64: + continue + else: + if board_list[current_pos + 9 - 1] == '0': + new_board = board_list[:current_pos - 1] + ['0'] + board_list[current_pos:current_pos + 9 - 1] + ['1'] + board_list[current_pos + 9:] + new_boat = [str(current_pos + 9)] + boat[1:] + list_to_return.append(self.hash(self.untranslate((''.join(new_board) + ''.join(new_boat))))) + + # move row + rowList = [] + for x in range(8): + rowList.append(string_rep[9*x: 9*x + 9]) + + for x in range(8): + if rowList[x][0] == '0': + modifiedRow = rowList[x][1:] + "0" + modifiedPos = ''.join(rowList[0:x]) + modifiedRow + ''.join(rowList[(x+1):]) + string_rep[72:] + + modifiedPos = self.untranslate(modifiedPos) + + + list_to_return.append(self.hash(modifiedPos)) + if rowList[x][8] == '0': + modifiedRow = "0" + rowList[x][:8] + modifiedPos = ''.join(rowList[0:x]) + modifiedRow + ''.join(rowList[(x+1):]) + string_rep[72:] + list_to_return.append(self.hash(self.untranslate((modifiedPos)))) + + # move boat left/right + for boat in boat_list: + current_dir = int(boat[2]) + current_length = int(boat[3]) + occupied_pos_1 = int(boat[0]) * 10 + int(boat[1]) + + if current_dir == 1 or current_dir == 3: + if current_dir == 1: + occupied_pos_2 = occupied_pos_1 + 9 + elif current_dir == 3: + occupied_pos_2 = occupied_pos_1 + occupied_pos_1 = occupied_pos_1 - 9 + + col_of_boat = occupied_pos_1 % 9 if occupied_pos_1 % 9 != 0 else 9 + col_of_boat_i = col_of_boat - 1 if col_of_boat != 9 else 8 + + row_1 = (occupied_pos_1 - 1) // 9 + row_2 = (occupied_pos_2 - 1) // 9 + + #moving left + if col_of_boat_i > 0 and rowList[row_1][0] == '0' and rowList[row_2][0] == '0': + modifiedRow1 = rowList[row_1][1:] + "0" + modifiedRow2 = rowList[row_2][1:] + "0" + modified_pos = occupied_pos_1 - 1 + modified_boat = [str(modified_pos)] + boat[1:] + + new_rowlist = rowList[:min(row_1, row_2)] + [modifiedRow1 if i == row_1 else modifiedRow2 if i == row_2 else rowList[i] for i in range(len(rowList))] + [modified_boat] + list_to_return.append(self.hash(self.untranslate(''.join(new_rowlist[0])))) + + #moving right + if col_of_boat_i < 8 and rowList[row_1][8] == '0' and rowList[row_2][8] == '0': + modifiedRow1 = "0" + rowList[row_1][:8] + modifiedRow2 = "0" + rowList[row_2][:8] + modified_pos = occupied_pos_1 + 1 + modified_boat = [str(modified_pos)] + boat[1:] + + new_rowlist = rowList[:min(row_1, row_2)] + [modifiedRow1 if i == row_1 else modifiedRow2 if i == row_2 else rowList[i] for i in range(len(rowList))] + [modified_boat] + list_to_return.append(self.hash(self.untranslate((''.join(new_rowlist[0]))))) + + return list_to_return + + def do_move(self, position: int, move: int) -> int: + """ + Returns the resulting position of applying move to position. + """ + # If move is already a position (from generate_moves), return it directly + # Otherwise, this could be an index into the moves list + possible_moves = self.generate_moves(position) + if isinstance(move, int) and 0 <= move < len(possible_moves): + return possible_moves[move] + return move + + def primitive(self, position: int) -> Optional[Value]: + """ + Returns a Value enum which defines whether the current position is a win, loss, or non-terminal. + """ + string_rep = self.translate(self.unhash(position)) + + # Check if any boat has reached position 69 facing down (direction 3) + boats_start = 72 + for i in range(0, len(string_rep) - boats_start, 4): + boat_data = string_rep[boats_start + i:boats_start + i + 4] + if len(boat_data) >= 4: + boat_pos = int(boat_data[:2]) + boat_dir = int(boat_data[2]) + if boat_pos == 69 and boat_dir == 3: + return Value.Win + + return None + + def to_string(self, position: int, mode: StringMode) -> str: + """ + Returns a string representation of the position based on the given mode. + """ + string_rep = self.translate(self.unhash(position)) + waveString = string_rep[:72] + + string_view = list(''.join(['.' if x == '0' else '~' for x in waveString])) # change these symbols + + boatString = string_rep[72:] + + i = 0 + color = 0 + while i <= len(boatString): + if i + 4 > len(boatString): + break + boatSlice = boatString[i:i+4] + boatPos = int(boatSlice[:2]) - 1 # Convert to index + + if boatPos < len(string_view): + string_view[boatPos] = self.colors[color] + + boat_dir = int(boatSlice[2]) + # Mark the other part of the boat + if boat_dir == 1 and boatPos + 9 < len(string_view): + string_view[boatPos + 9] = self.colors[color].lower() + elif boat_dir == 2 and boatPos - 1 >= 0: + string_view[boatPos - 1] = self.colors[color].lower() + elif boat_dir == 3 and boatPos - 9 >= 0: + string_view[boatPos - 9] = self.colors[color].lower() + elif boat_dir == 4 and boatPos + 1 < len(string_view): + string_view[boatPos + 1] = self.colors[color].lower() + color += 1 + i += 4 + + string_view = ''.join(string_view) + return "\n".join([string_view[9*x: 9*x + 9] for x in range(8)]) + + def from_string(self, strposition: str) -> int: + """ + Returns the position from a string representation of the position. + Input string is StringMode.Readable. + """ + # Remove newlines and convert readable symbols back to binary + cleaned = strposition.replace("\n", "") + binary_str = ''.join(['0' if c == '.' else '1' for c in cleaned if c in ['.', '~']]) + + # Extract boat information from remaining characters + boat_chars = [c for c in cleaned if c in self.colors or c in self.colors[0].lower()] + + # Convert boat characters to boat data (position, direction, length) + boat_info = "" + boat_positions = {} + + for idx, char in enumerate(cleaned): + if char.upper() in self.colors: + if char.upper() not in boat_positions: + boat_positions[char.upper()] = [] + boat_positions[char.upper()].append((idx + 1, char.isupper())) # 1-indexed position, isupper=start + + # Build boat info string (4 digits per boat: position + direction + length) + for color in self.colors: + if color in boat_positions: + positions = sorted(boat_positions[color]) + start_pos = min(p[0] for p in positions) + + # Determine direction and length + if len(positions) > 1: + pos_list = [p[0] for p in positions] + if pos_list[1] - pos_list[0] == 9: # vertical + direction = 1 if positions[0][1] else 3 # 1=up, 3=down + else: # horizontal + direction = 2 if positions[0][1] else 4 # 2=right, 4=left + else: # single boat piece + direction = 1 if positions[0][1] else 3 + length = len(positions) + + boat_info += f"{start_pos:02d}{direction}{length}" + + return self.hash(self.translate((binary_str + boat_info))) + + def move_to_string(self, move: int, mode: StringMode) -> str: + """ + Returns a string representation of the move based on the given mode. + """ + if mode == StringMode.Readable: + return self.to_string(move, mode) + return str(move) + + def hash(self, strPos: str) -> int: + """ + Converts a string position to an integer hash. + """ + + # first 8 characters of strPos is the ternary rep of waves + # rest are regular numbers, first convert these to ternary via boatTernary + # put string back together, then the final string convert into an integer via int(x, 3) + + wavePosString = strPos[:8] + boatString = strPos[8:] + boatTernaryString = "" + + for i in range(0, len(boatString), 4): + boat = boatString[i:i+4] + boatTernaryString += self.boatTernary(boat) + + result = wavePosString + boatTernaryString + + return int(result, 3) + + def unhash(self, intPos: int) -> str: + """ + Converts an integer hash back to a string position. + """ + + + #turn integer back into ternary string VIA to ternary + #first 8 digits are fine, boats are seperated via length 8 and turned back into their integer reps + #then add everything together again + + strPos = str(self.toTernaryString(intPos)) + + wavePosString = strPos[:8] + boatTernaryString = strPos[8:] + boatString = "" + + while boatTernaryString != "": + boatString += self.boatTernaryReverse(boatTernaryString[:8]) + boatTernaryString = boatTernaryString[8:] + + return wavePosString + boatString + + def boatTernary(self, boatString: str) -> str: + #turn the (string) integer rep of boats into ternary strings + + boatPos = self.toTernaryString(int(boatString[:2])).rjust(4, "0") + boatDir = self.toTernaryString(int(boatString[2])).rjust(2, "0") + boatLen = self.toTernaryString(int(boatString[3])).rjust(2, "0") + + result = boatPos + boatDir + boatLen + + return result + + def boatTernaryReverse(self, boatTernaryStr: str) -> str: + #turn ternary string representations back into boat integers (but string) + + return str(int(boatTernaryStr[:4], 3)) + str(int(boatTernaryStr[4:6], 3)) + str(int(boatTernaryStr[6:], 3)) + + + def toTernaryString(self, n): + if n == 0: + return "0" + + tern_digits = [] + while n: + remainder = n % 3 + tern_digits.append(str(remainder)) + n = n // 3 + + tern_str = "".join(tern_digits[::-1]) + + return tern_str + + def translate(self, str): + #turn the shifts into a board :sob: + + + shifts = [x for x in str[:8]] + board = "" + row = 0 + + for x in shifts: + if x == '0': + board = board + self.board_rows[row] + elif x == '1': + board += "0" + self.board_rows[row][:8] + else: + board += "00" + self.board_rows[row][:7] + + + row += 1 + + + return board + str[8:] + + def untranslate(self, str): + + + board_part = str[:72] + boat_part = str[72:] + + shifts = "" + for row_idx in range(8): + row = board_part[row_idx * 9 : (row_idx + 1) * 9] + expected_row = self.board_rows[row_idx] + + if row == expected_row: + shifts += "0" + elif row == "0" + expected_row[:8]: + shifts += "1" + elif row == "00" + expected_row[:7]: + shifts += "2" + + + return shifts + boat_part + + + + + + diff --git a/games/src/games/game_manager.py b/games/src/games/game_manager.py index 464da01..670f977 100644 --- a/games/src/games/game_manager.py +++ b/games/src/games/game_manager.py @@ -2,6 +2,7 @@ from .horses import Horses from .pancakes import Pancakes from .test import Test +from .StormySeasV3 import StormySeas from models import * game_list = { @@ -9,6 +10,7 @@ "horses": Horses, "pancakes": Pancakes, "test": Test, + "stormyseas": StormySeas } def validate(game_id: str, variant_id: str) -> bool: From 126f545e559cccb07a1f50bf9f404f465effef9e Mon Sep 17 00:00:00 2001 From: Sora Wongsonegoro Date: Sun, 8 Mar 2026 17:38:37 -0700 Subject: [PATCH 10/29] updated stormy seas --- games/src/games/StormySeasV3.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/games/src/games/StormySeasV3.py b/games/src/games/StormySeasV3.py index cb4aefc..85ab102 100644 --- a/games/src/games/StormySeasV3.py +++ b/games/src/games/StormySeasV3.py @@ -15,7 +15,7 @@ def __init__(self, variant_id: str): if variant_id not in StormySeas.variants: raise ValueError("Variant not defined") self._variant_id = variant_id - board_rows = [] + self.board_rows = [] def start(self) -> int: if self._variant_id == "a": @@ -28,12 +28,22 @@ def start(self) -> int: return hash elif self._variant_id == "b": # Add variant b starting position - string_rep = "010101111001011101010110110010101110010101110001011101000011011011110111001123238221142" - return self.hash(string_rep) + self.board_rows = ["101011100","101110100","101101100","101011100","101011100","101110100","110110100","111011100"] + + # use ternary digits to represent shifts? + curr_shift_string = "11000020" + boat_pos = "123256227142" # first two digits are location, third digit is direction (1 = up, 2 = right, 3 = down, 4 = left), last digit is length + hash = self.hash(curr_shift_string + boat_pos) + return hash elif self._variant_id == "c": # Add variant c starting position - string_rep = "010101111001011101010110110010101110010101110001011101000011011011110111001123238221142" - return self.hash(string_rep) + self.board_rows = ["101011100","101110100","101101100","101011100","101011100","101110100","110110100","111011100"] + + # use ternary digits to represent shifts? + curr_shift_string = "11000020" + boat_pos = "123256227142" # first two digits are location, third digit is direction (1 = up, 2 = right, 3 = down, 4 = left), last digit is length + hash = self.hash(curr_shift_string + boat_pos) + return hash return 0 From 5d20aa5ed2d7e0ae9e99a0999e57ed15b43df19d Mon Sep 17 00:00:00 2001 From: Sora Wongsonegoro Date: Thu, 23 Apr 2026 23:09:27 -0700 Subject: [PATCH 11/29] Stormy Seas fixed --- games/src/games/StormySeasV3.py | 388 ------------------------- games/src/games/game_manager.py | 2 +- games/src/games/stormyseassmall.py | 450 +++++++++++++++++++++++++++++ 3 files changed, 451 insertions(+), 389 deletions(-) delete mode 100644 games/src/games/StormySeasV3.py create mode 100644 games/src/games/stormyseassmall.py diff --git a/games/src/games/StormySeasV3.py b/games/src/games/StormySeasV3.py deleted file mode 100644 index 85ab102..0000000 --- a/games/src/games/StormySeasV3.py +++ /dev/null @@ -1,388 +0,0 @@ -from models import Game, Value, StringMode -from typing import Optional - -class StormySeas(Game): - id = 'stormyseas' - variants = ["a", "b", "c"] - n_players = 1 - cyclic = False - colors = ["R", "G", "O"] - - def __init__(self, variant_id: str): - """ - Define instance variables here (i.e. variant information) - """ - if variant_id not in StormySeas.variants: - raise ValueError("Variant not defined") - self._variant_id = variant_id - self.board_rows = [] - - def start(self) -> int: - if self._variant_id == "a": - self.board_rows = ["101011100","101110100","101101100","101011100","101011100","101110100","110110100","111011100"] - - # use ternary digits to represent shifts? - curr_shift_string = "11000020" - boat_pos = "123256227142" # first two digits are location, third digit is direction (1 = up, 2 = right, 3 = down, 4 = left), last digit is length - hash = self.hash(curr_shift_string + boat_pos) - return hash - elif self._variant_id == "b": - # Add variant b starting position - self.board_rows = ["101011100","101110100","101101100","101011100","101011100","101110100","110110100","111011100"] - - # use ternary digits to represent shifts? - curr_shift_string = "11000020" - boat_pos = "123256227142" # first two digits are location, third digit is direction (1 = up, 2 = right, 3 = down, 4 = left), last digit is length - hash = self.hash(curr_shift_string + boat_pos) - return hash - elif self._variant_id == "c": - # Add variant c starting position - self.board_rows = ["101011100","101110100","101101100","101011100","101011100","101110100","110110100","111011100"] - - # use ternary digits to represent shifts? - curr_shift_string = "11000020" - boat_pos = "123256227142" # first two digits are location, third digit is direction (1 = up, 2 = right, 3 = down, 4 = left), last digit is length - hash = self.hash(curr_shift_string + boat_pos) - return hash - - return 0 - - def generate_moves(self, position: int): - """ - Returns a list of positions given the input position. - """ - string_rep = self.translate(self.unhash(position)) - list_to_return = [] - - string_rep_list = list(string_rep) - - #DO NOT MODIFY THESE POSITIONS DIRECTLY - board_list = string_rep_list[:72] - boat_1 = string_rep_list[72:76] - boat_2 = string_rep_list[76:80] - boat_3 = string_rep_list[80:84] - boat_list = [boat_1, boat_2, boat_3] - - #up - for boat in boat_list: - current_pos = int(boat[0]) * 10 + int(boat[1]) - current_dir = int(boat[2]) - - #up - if current_dir == 1: - if current_pos <= 9: - continue - elif board_list[current_pos - 9 - 1] == '0': - new_board = board_list[:current_pos - 9 - 1] + ['1'] + board_list[current_pos - 9:current_pos - 1] + ['0'] + board_list[current_pos:] - new_boat = [str(current_pos - 9)] + boat[1:] - list_to_return.append(self.hash(self.untranslate((''.join(new_board) + ''.join(new_boat))))) - #down - elif current_dir == 3: - if current_pos >= 64: - continue - else: - if board_list[current_pos + 9 - 1] == '0': - new_board = board_list[:current_pos - 1] + ['0'] + board_list[current_pos:current_pos + 9 - 1] + ['1'] + board_list[current_pos + 9:] - new_boat = [str(current_pos + 9)] + boat[1:] - list_to_return.append(self.hash(self.untranslate((''.join(new_board) + ''.join(new_boat))))) - - # move row - rowList = [] - for x in range(8): - rowList.append(string_rep[9*x: 9*x + 9]) - - for x in range(8): - if rowList[x][0] == '0': - modifiedRow = rowList[x][1:] + "0" - modifiedPos = ''.join(rowList[0:x]) + modifiedRow + ''.join(rowList[(x+1):]) + string_rep[72:] - - modifiedPos = self.untranslate(modifiedPos) - - - list_to_return.append(self.hash(modifiedPos)) - if rowList[x][8] == '0': - modifiedRow = "0" + rowList[x][:8] - modifiedPos = ''.join(rowList[0:x]) + modifiedRow + ''.join(rowList[(x+1):]) + string_rep[72:] - list_to_return.append(self.hash(self.untranslate((modifiedPos)))) - - # move boat left/right - for boat in boat_list: - current_dir = int(boat[2]) - current_length = int(boat[3]) - occupied_pos_1 = int(boat[0]) * 10 + int(boat[1]) - - if current_dir == 1 or current_dir == 3: - if current_dir == 1: - occupied_pos_2 = occupied_pos_1 + 9 - elif current_dir == 3: - occupied_pos_2 = occupied_pos_1 - occupied_pos_1 = occupied_pos_1 - 9 - - col_of_boat = occupied_pos_1 % 9 if occupied_pos_1 % 9 != 0 else 9 - col_of_boat_i = col_of_boat - 1 if col_of_boat != 9 else 8 - - row_1 = (occupied_pos_1 - 1) // 9 - row_2 = (occupied_pos_2 - 1) // 9 - - #moving left - if col_of_boat_i > 0 and rowList[row_1][0] == '0' and rowList[row_2][0] == '0': - modifiedRow1 = rowList[row_1][1:] + "0" - modifiedRow2 = rowList[row_2][1:] + "0" - modified_pos = occupied_pos_1 - 1 - modified_boat = [str(modified_pos)] + boat[1:] - - new_rowlist = rowList[:min(row_1, row_2)] + [modifiedRow1 if i == row_1 else modifiedRow2 if i == row_2 else rowList[i] for i in range(len(rowList))] + [modified_boat] - list_to_return.append(self.hash(self.untranslate(''.join(new_rowlist[0])))) - - #moving right - if col_of_boat_i < 8 and rowList[row_1][8] == '0' and rowList[row_2][8] == '0': - modifiedRow1 = "0" + rowList[row_1][:8] - modifiedRow2 = "0" + rowList[row_2][:8] - modified_pos = occupied_pos_1 + 1 - modified_boat = [str(modified_pos)] + boat[1:] - - new_rowlist = rowList[:min(row_1, row_2)] + [modifiedRow1 if i == row_1 else modifiedRow2 if i == row_2 else rowList[i] for i in range(len(rowList))] + [modified_boat] - list_to_return.append(self.hash(self.untranslate((''.join(new_rowlist[0]))))) - - return list_to_return - - def do_move(self, position: int, move: int) -> int: - """ - Returns the resulting position of applying move to position. - """ - # If move is already a position (from generate_moves), return it directly - # Otherwise, this could be an index into the moves list - possible_moves = self.generate_moves(position) - if isinstance(move, int) and 0 <= move < len(possible_moves): - return possible_moves[move] - return move - - def primitive(self, position: int) -> Optional[Value]: - """ - Returns a Value enum which defines whether the current position is a win, loss, or non-terminal. - """ - string_rep = self.translate(self.unhash(position)) - - # Check if any boat has reached position 69 facing down (direction 3) - boats_start = 72 - for i in range(0, len(string_rep) - boats_start, 4): - boat_data = string_rep[boats_start + i:boats_start + i + 4] - if len(boat_data) >= 4: - boat_pos = int(boat_data[:2]) - boat_dir = int(boat_data[2]) - if boat_pos == 69 and boat_dir == 3: - return Value.Win - - return None - - def to_string(self, position: int, mode: StringMode) -> str: - """ - Returns a string representation of the position based on the given mode. - """ - string_rep = self.translate(self.unhash(position)) - waveString = string_rep[:72] - - string_view = list(''.join(['.' if x == '0' else '~' for x in waveString])) # change these symbols - - boatString = string_rep[72:] - - i = 0 - color = 0 - while i <= len(boatString): - if i + 4 > len(boatString): - break - boatSlice = boatString[i:i+4] - boatPos = int(boatSlice[:2]) - 1 # Convert to index - - if boatPos < len(string_view): - string_view[boatPos] = self.colors[color] - - boat_dir = int(boatSlice[2]) - # Mark the other part of the boat - if boat_dir == 1 and boatPos + 9 < len(string_view): - string_view[boatPos + 9] = self.colors[color].lower() - elif boat_dir == 2 and boatPos - 1 >= 0: - string_view[boatPos - 1] = self.colors[color].lower() - elif boat_dir == 3 and boatPos - 9 >= 0: - string_view[boatPos - 9] = self.colors[color].lower() - elif boat_dir == 4 and boatPos + 1 < len(string_view): - string_view[boatPos + 1] = self.colors[color].lower() - color += 1 - i += 4 - - string_view = ''.join(string_view) - return "\n".join([string_view[9*x: 9*x + 9] for x in range(8)]) - - def from_string(self, strposition: str) -> int: - """ - Returns the position from a string representation of the position. - Input string is StringMode.Readable. - """ - # Remove newlines and convert readable symbols back to binary - cleaned = strposition.replace("\n", "") - binary_str = ''.join(['0' if c == '.' else '1' for c in cleaned if c in ['.', '~']]) - - # Extract boat information from remaining characters - boat_chars = [c for c in cleaned if c in self.colors or c in self.colors[0].lower()] - - # Convert boat characters to boat data (position, direction, length) - boat_info = "" - boat_positions = {} - - for idx, char in enumerate(cleaned): - if char.upper() in self.colors: - if char.upper() not in boat_positions: - boat_positions[char.upper()] = [] - boat_positions[char.upper()].append((idx + 1, char.isupper())) # 1-indexed position, isupper=start - - # Build boat info string (4 digits per boat: position + direction + length) - for color in self.colors: - if color in boat_positions: - positions = sorted(boat_positions[color]) - start_pos = min(p[0] for p in positions) - - # Determine direction and length - if len(positions) > 1: - pos_list = [p[0] for p in positions] - if pos_list[1] - pos_list[0] == 9: # vertical - direction = 1 if positions[0][1] else 3 # 1=up, 3=down - else: # horizontal - direction = 2 if positions[0][1] else 4 # 2=right, 4=left - else: # single boat piece - direction = 1 if positions[0][1] else 3 - length = len(positions) - - boat_info += f"{start_pos:02d}{direction}{length}" - - return self.hash(self.translate((binary_str + boat_info))) - - def move_to_string(self, move: int, mode: StringMode) -> str: - """ - Returns a string representation of the move based on the given mode. - """ - if mode == StringMode.Readable: - return self.to_string(move, mode) - return str(move) - - def hash(self, strPos: str) -> int: - """ - Converts a string position to an integer hash. - """ - - # first 8 characters of strPos is the ternary rep of waves - # rest are regular numbers, first convert these to ternary via boatTernary - # put string back together, then the final string convert into an integer via int(x, 3) - - wavePosString = strPos[:8] - boatString = strPos[8:] - boatTernaryString = "" - - for i in range(0, len(boatString), 4): - boat = boatString[i:i+4] - boatTernaryString += self.boatTernary(boat) - - result = wavePosString + boatTernaryString - - return int(result, 3) - - def unhash(self, intPos: int) -> str: - """ - Converts an integer hash back to a string position. - """ - - - #turn integer back into ternary string VIA to ternary - #first 8 digits are fine, boats are seperated via length 8 and turned back into their integer reps - #then add everything together again - - strPos = str(self.toTernaryString(intPos)) - - wavePosString = strPos[:8] - boatTernaryString = strPos[8:] - boatString = "" - - while boatTernaryString != "": - boatString += self.boatTernaryReverse(boatTernaryString[:8]) - boatTernaryString = boatTernaryString[8:] - - return wavePosString + boatString - - def boatTernary(self, boatString: str) -> str: - #turn the (string) integer rep of boats into ternary strings - - boatPos = self.toTernaryString(int(boatString[:2])).rjust(4, "0") - boatDir = self.toTernaryString(int(boatString[2])).rjust(2, "0") - boatLen = self.toTernaryString(int(boatString[3])).rjust(2, "0") - - result = boatPos + boatDir + boatLen - - return result - - def boatTernaryReverse(self, boatTernaryStr: str) -> str: - #turn ternary string representations back into boat integers (but string) - - return str(int(boatTernaryStr[:4], 3)) + str(int(boatTernaryStr[4:6], 3)) + str(int(boatTernaryStr[6:], 3)) - - - def toTernaryString(self, n): - if n == 0: - return "0" - - tern_digits = [] - while n: - remainder = n % 3 - tern_digits.append(str(remainder)) - n = n // 3 - - tern_str = "".join(tern_digits[::-1]) - - return tern_str - - def translate(self, str): - #turn the shifts into a board :sob: - - - shifts = [x for x in str[:8]] - board = "" - row = 0 - - for x in shifts: - if x == '0': - board = board + self.board_rows[row] - elif x == '1': - board += "0" + self.board_rows[row][:8] - else: - board += "00" + self.board_rows[row][:7] - - - row += 1 - - - return board + str[8:] - - def untranslate(self, str): - - - board_part = str[:72] - boat_part = str[72:] - - shifts = "" - for row_idx in range(8): - row = board_part[row_idx * 9 : (row_idx + 1) * 9] - expected_row = self.board_rows[row_idx] - - if row == expected_row: - shifts += "0" - elif row == "0" + expected_row[:8]: - shifts += "1" - elif row == "00" + expected_row[:7]: - shifts += "2" - - - return shifts + boat_part - - - - - - diff --git a/games/src/games/game_manager.py b/games/src/games/game_manager.py index 670f977..73433be 100644 --- a/games/src/games/game_manager.py +++ b/games/src/games/game_manager.py @@ -2,7 +2,7 @@ from .horses import Horses from .pancakes import Pancakes from .test import Test -from .StormySeasV3 import StormySeas +from .stormyseassmall import StormySeas from models import * game_list = { diff --git a/games/src/games/stormyseassmall.py b/games/src/games/stormyseassmall.py new file mode 100644 index 0000000..089e8d5 --- /dev/null +++ b/games/src/games/stormyseassmall.py @@ -0,0 +1,450 @@ +from models import Game, Value, StringMode +from typing import Optional + +class StormySeas(Game): + id = 'stormyseas' + variants = ["a"] + n_players = 1 + cyclic = False + colors = ["R", "B"] + + def __init__(self, variant_id: str): + """ + Define instance variables here (i.e. variant information) + """ + if variant_id not in StormySeas.variants: + raise ValueError("Variant not defined") + self._variant_id = variant_id + self.board_rows = [] + self.default_rows = [] + self.boat_pos = [] + self.row_length = 0 + self.num_rows = 0 + self.win_condition = "" # Example win condition + + def start(self) -> int: + if self._variant_id == "a": + self.default_rows = ["1011100","1010100","1101100","1011100","1110100"] + self.board_rows = ["0101110","0101010","0011011","0101110","0111010"] + + # use ternary digits to represent shifts? + curr_shift_string = "11211" + boat_pos = ["24", "12"] # first two digits is position in x-y where 0, 0 is top left square on board (always facing DOWN and always length 2) + self.boat_pos = boat_pos + + self.row_length = len(self.board_rows[0]) + self.num_rows = len(self.board_rows) + self.win_condition = "43" + + + for x in boat_pos: + curr_shift_string += x + + hash = self.hash(curr_shift_string) #need to change/account for + return hash + + return 0 + + def rowsWithBoats(self): + rows_w_boats = [] + for i in range(len(self.boat_pos)): + pos_1 = int(self.boat_pos[i][0]) + pos_2 = pos_1 - 1 + if pos_1 not in rows_w_boats: + rows_w_boats.append(pos_1) + if pos_2 >= 0 and pos_2 not in rows_w_boats: # guard against -1 + rows_w_boats.append(pos_2) + return rows_w_boats + + + def rowsMoveableLeft(self): + rows_moveable_left = list(range(0, self.num_rows)) + for i in range(len(self.board_rows)): + if int(self.board_rows[i][0]) == 1: + rows_moveable_left.remove(i) + + return rows_moveable_left + + + def rowsMoveableRight(self): + rows_moveable_right = list(range(0, self.num_rows)) + for i in range(0, self.num_rows): + if int(self.board_rows[i][self.row_length - 1]) == 1: + rows_moveable_right.remove(i) + + return rows_moveable_right + + + def overlappingRows(self): + overlapping_rows = [] + # Compare as ints, not strings + b0_row = int(self.boat_pos[0][0]) + b1_row = int(self.boat_pos[1][0]) + + # Boats overlap if their occupied rows intersect + # boat occupies rows: [bottom, bottom-1] + b0_rows = {b0_row, b0_row - 1} + b1_rows = {b1_row, b1_row - 1} + + if b0_rows & b1_rows: # non-empty intersection + overlapping_rows = list(b0_rows | b1_rows) + + return overlapping_rows + + def moveRowsRight(self, rows): + stringToReturn = "" + for orig_row_i in range(0, self.num_rows): + if orig_row_i in rows: + stringToReturn += "0" + self.board_rows[orig_row_i][:self.row_length - 1] + else: + stringToReturn += self.board_rows[orig_row_i] + + return stringToReturn + + def moveRowsLeft(self,rows): + stringToReturn = "" + for orig_row_i in range(0, self.num_rows): + if orig_row_i in rows: + stringToReturn += self.board_rows[orig_row_i][1:] + "0" + else: + stringToReturn += self.board_rows[orig_row_i] + + return stringToReturn + + def returnRows(self): + stringToReturn = "" + + for orig_row in self.board_rows: + stringToReturn += orig_row + + return stringToReturn + + + def generate_moves(self, position: int): + string_rep = self.translate(self.unhash(position)) + self.board_rows = [string_rep[i:i+7] for i in range(0, 35, 7)] + # Also update boat_pos from the position + boat_str = string_rep[35:] + self.boat_pos = [boat_str[i:i+2] for i in range(0, len(boat_str), 2)] + + positions = [] + leftable_rows = self.rowsMoveableLeft() + rightable_rows = self.rowsMoveableRight() + touchable_rows = self.rowsWithBoats() + overlapping_rows = self.overlappingRows() + + # 1. Move any non-boat row left + for row in range(0, self.num_rows): + if row not in touchable_rows and row in leftable_rows: + stringToReturn = self.moveRowsLeft([row]) + for bp in self.boat_pos: + stringToReturn += bp + positions.append(self.hash(self.untranslate(stringToReturn))) + + # 2. Move any non-boat row right + for row in range(0, self.num_rows): + if row not in touchable_rows and row in rightable_rows: + stringToReturn = self.moveRowsRight([row]) + for bp in self.boat_pos: + stringToReturn += bp + positions.append(self.hash(self.untranslate(stringToReturn))) + + # 3. Move any boat left (rows move left, boat col decreases) + if overlapping_rows: + if all(row in leftable_rows for row in overlapping_rows): + stringToReturn = self.moveRowsLeft(overlapping_rows) + for bp in self.boat_pos: + col = int(bp[1]) - 1 + if col < 0: + col = self.row_length - 1 + stringToReturn += bp[0] + str(col) + positions.append(self.hash(self.untranslate(stringToReturn))) + else: + for boat_i in range(len(self.boat_pos)): + bp = self.boat_pos[boat_i] + rows = [int(bp[0]), int(bp[0]) - 1] + if all(row in leftable_rows for row in rows) and int(bp[1]) > 0: + stringToReturn = self.moveRowsLeft(rows) + for j, other_bp in enumerate(self.boat_pos): + if j == boat_i: + stringToReturn += other_bp[0] + str(int(other_bp[1]) - 1) + else: + stringToReturn += other_bp + positions.append(self.hash(self.untranslate(stringToReturn))) + + # 4. Move any boat right (rows move right, boat col increases) + if overlapping_rows: + if all(row in rightable_rows for row in overlapping_rows): + stringToReturn = self.moveRowsRight(overlapping_rows) + for bp in self.boat_pos: + col = int(bp[1]) + 1 + if col > self.row_length - 1: + col = 0 + stringToReturn += bp[0] + str(col) + positions.append(self.hash(self.untranslate(stringToReturn))) + else: + for boat_i in range(len(self.boat_pos)): + bp = self.boat_pos[boat_i] + rows = [int(bp[0]), int(bp[0]) - 1] + if all(row in rightable_rows for row in rows) and int(bp[1]) < self.row_length - 1: + stringToReturn = self.moveRowsRight(rows) + for j, other_bp in enumerate(self.boat_pos): + if j == boat_i: + stringToReturn += other_bp[0] + str(int(other_bp[1]) + 1) + else: + stringToReturn += other_bp + positions.append(self.hash(self.untranslate(stringToReturn))) + + # 5. Slide any boat up or down + for boat_i in range(len(self.boat_pos)): + bp = self.boat_pos[boat_i] + row = int(bp[0]) + col = int(bp[1]) + + # The board is translated (shifted), so col indexes correctly into board_rows + # Move down: check row+1 exists and is empty + if row < self.num_rows - 1 and self.board_rows[row + 1][col] == '0': + stringToReturn = self.returnRows() + for j, other_bp in enumerate(self.boat_pos): + if j == boat_i: + stringToReturn += str(row + 1) + str(col) + else: + stringToReturn += other_bp + positions.append(self.hash(self.untranslate(stringToReturn))) + + # Move up: top cell is at row-1, new top would be row-2 + if row > 0 and self.board_rows[row - 2][col] == '0': + stringToReturn = self.returnRows() + for j, other_bp in enumerate(self.boat_pos): + if j == boat_i: + stringToReturn += str(row - 1) + str(col) + else: + stringToReturn += other_bp + positions.append(self.hash(self.untranslate(stringToReturn))) + + return positions + + def do_move(self, position: int, move: int) -> int: + """ + Returns the resulting position of applying move to position. + """ + # If move is already a position (from generate_moves), return it directly + # Otherwise, this could be an index into the moves list + possible_moves = self.generate_moves(position) + if isinstance(move, int) and 0 <= move < len(possible_moves): + return possible_moves[move] + return move + + def primitive(self, position: int) -> Optional[Value]: + """ + Returns a Value enum which defines whether the current position is a win, loss, or non-terminal. + """ + string_rep = self.translate(self.unhash(position)) + + boats_start = 35 + if (string_rep[(self.row_length * self.num_rows):(self.row_length * self.num_rows + 2)] == self.win_condition): + return Value.Win + return None + + def CoordinateToPosition(self, x: int, y: int) -> int: + return x + y * self.row_length # board is row_length columns wide + + def to_string(self, position: int, mode: StringMode) -> str: + """ + Returns a string representation of the position based on the given mode. + """ + string_rep = self.translate(self.unhash(position)) + waveString = string_rep[:self.row_length * self.num_rows] + boatString = string_rep[self.row_length * self.num_rows:] + + # Build base grid from wave data + string_view = list(''.join(['~' if x == '1' else '.' for x in waveString])) + + i = 0 + color = 0 + while i + 2 <= len(boatString): + boatSlice = boatString[i:i+2] + row = int(boatSlice[0]) # bottom cell row + col = int(boatSlice[1]) # column + + bottom_idx = self.CoordinateToPosition(col, row) + top_idx = self.CoordinateToPosition(col, row - 1) + + if 0 <= bottom_idx < self.row_length * self.num_rows and 0 <= top_idx < self.row_length * self.num_rows: + string_view[bottom_idx] = self.colors[color].upper() # bottom = uppercase + string_view[top_idx] = self.colors[color].lower() # top = lowercase + + color += 1 + i += 2 + + string_view = ''.join(string_view) + return "\n".join([string_view[self.row_length*x: self.row_length*x + self.row_length] for x in range(self.num_rows)]) + + def from_string(self, strposition: str) -> int: + """ + Returns the position from a string representation of the position. + Input string is StringMode.Readable. + """ + rows = strposition.split("\n") + binary_str = "" + boat_positions = {} # color -> (row, col) of bottom cell + + for row_idx, row in enumerate(rows): + for col_idx, char in enumerate(row): + if char == '~': + binary_str += '1' + elif char == '.': + binary_str += '0' + elif char.upper() in self.colors: + binary_str += '0' # boat cell is not a wave + if char.isupper(): # uppercase = bottom cell + boat_positions[char.upper()] = (row_idx, col_idx) + else: + binary_str += '0' # fallback + + # Build boat_pos list in the same order as self.colors + boat_pos_list = [] + for color in self.colors: + if color in boat_positions: + row, col = boat_positions[color] + boat_pos_list.append(f"{row}{col}") + + # Combine binary board + boat positions, then untranslate and hash + full_string = binary_str + "".join(boat_pos_list) + return self.hash(self.untranslate(full_string)) + + + def move_to_string(self, move: int, mode: StringMode) -> str: + if mode != StringMode.Readable: + return str(move) + + # Get current board state from self.board_rows and self.boat_pos + # (already set by the most recent generate_moves call) + move_rep = self.translate(self.unhash(move)) + move_rows = [move_rep[i:i+self.row_length] for i in range(0, self.row_length * self.num_rows, self.row_length)] + move_boat_str = move_rep[self.row_length * self.num_rows:] + move_boats = [move_boat_str[i:i+2] for i in range(0, len(move_boat_str), 2)] + + curr_rows = self.board_rows + curr_boats = self.boat_pos + + # Check if any boat moved + for boat_i, (curr_bp, move_bp) in enumerate(zip(curr_boats, move_boats)): + curr_row, curr_col = int(curr_bp[0]), int(curr_bp[1]) + move_row, move_col = int(move_bp[0]), int(move_bp[1]) + color = self.colors[boat_i].lower() + + if curr_col != move_col or curr_row != move_row: + if curr_row != move_row: + direction = "down" if move_row > curr_row else "up" + return f"boat{color}-{direction}" + else: + direction = "left" if move_col < curr_col else "right" + return f"boat{color}-{direction}" + + # Otherwise a wave row moved + for row_i, (curr_row, move_row) in enumerate(zip(curr_rows, move_rows)): + if curr_row != move_row: + if move_row == curr_row[1:] + "0": + return f"row{row_i}-left" + else: + return f"row{row_i}-right" + + return str(move) # fallback + + def hash(self, strPos: str) -> int: + """ + Converts a string position to an integer hash. + """ + + # first 8 characters of strPos is the ternary rep of waves + # rest are regular numbers, first convert these to ternary via boatTernary + # put string back together, then the final string convert into an integer via int(x, 3) + + wavePosString = strPos[:self.num_rows] + boatString = strPos[self.num_rows:] + boatTernaryString = "" + + for i in range(0, len(boatString), 2): + boat = boatString[i:i+2] + boatTernaryString += self.boatTernary(boat) + + result = wavePosString + boatTernaryString + + return int(result, 3) + + def unhash(self, intPos: int) -> str: + total_length = self.num_rows + 5 * len(self.boat_pos) # 5 shifts + 5 ternary digits per boat + strPos = self.toTernaryString(intPos).rjust(total_length, "0") + + wavePosString = strPos[:self.num_rows] + boatTernaryString = strPos[self.num_rows:] + boatString = "" + + while boatTernaryString != "": + boatString += self.boatTernaryReverse(boatTernaryString[:5]) + boatTernaryString = boatTernaryString[5:] + + return wavePosString + boatString + + def boatTernary(self, boatString: str) -> str: + # Convert 2-digit decimal boat position to fixed 5-digit ternary + # max boat position is 46 (row 4, col 6) = ternary "1201" = 4 digits, so 5 is safe + boatPos = self.toTernaryString(int(boatString)).rjust(5, "0") + return boatPos + + def boatTernaryReverse(self, boatTernaryStr: str) -> str: + # Convert 5-digit ternary back to 2-digit decimal string, zero-padded + return str(int(boatTernaryStr, 3)).rjust(2, "0") + + + def toTernaryString(self, n): + if n == 0: + return "0" + + tern_digits = [] + while n: + remainder = n % 3 + tern_digits.append(str(remainder)) + n = n // 3 + + tern_str = "".join(tern_digits[::-1]) + + return tern_str + + def translate(self, str): + #turn the shifts into a board :sob: + + + shifts = [x for x in str[:self.num_rows]] + board = "" + for row, x in enumerate(shifts): + if x == '0': + board = board + self.default_rows[row] + elif x == '1': + board += "0" + self.default_rows[row][:self.row_length - 1] + else: + board += "00" + self.default_rows[row][:self.row_length - 2] + + return board + str[self.num_rows:] + + def untranslate(self, str): + + + board_part = str[:(self.num_rows * self.row_length)] + boat_part = str[self.num_rows * self.row_length:] + + shifts = "" + for row_idx in range(self.num_rows): + row = board_part[row_idx * self.row_length : (row_idx + 1) * self.row_length] + expected_row = self.default_rows[row_idx] + + if row == expected_row: + shifts += "0" + elif row == "0" + expected_row[:self.row_length - 1]: + shifts += "1" + elif row == "00" + expected_row[:self.row_length - 2]: + shifts += "2" + + + return shifts + boat_part \ No newline at end of file From fd7474eafcd2f29c30bc1ae0c91f43141e990a6d Mon Sep 17 00:00:00 2001 From: Manlin Zhang Date: Wed, 29 Apr 2026 12:42:53 -0700 Subject: [PATCH 12/29] added the stormyseas.py file from gc internal --- games/src/games/stormyseas.py | 500 ++++++++++++++++++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 games/src/games/stormyseas.py diff --git a/games/src/games/stormyseas.py b/games/src/games/stormyseas.py new file mode 100644 index 0000000..547ffa4 --- /dev/null +++ b/games/src/games/stormyseas.py @@ -0,0 +1,500 @@ +from models import Game, Value, StringMode +from typing import Optional + +class StormySeas(Game): + id = 'stormyseas' + variants = ["a", "regular"] # 'regular' maps to 'a' in the API + n_players = 1 + cyclic = False + colors = ["R", "B"] + + def __init__(self, variant_id: str): + """ + Define instance variables here (i.e. variant information) + """ + if variant_id not in StormySeas.variants: + raise ValueError("Variant not defined") + self._variant_id = variant_id + self.board_rows = [] + self.default_rows = [] + self.boat_pos = [] + self.row_length = 0 + self.num_rows = 0 + self.win_condition = "" # Example win condition + + def start(self) -> int: + if self._variant_id in ("a", "regular"): + self.default_rows = ["1011100","1010100","1101100","1011100","1110100"] + self.board_rows = ["0101110","0101010","0011011","0101110","0111010"] + + # use ternary digits to represent shifts? + curr_shift_string = "11211" + boat_pos = ["24", "12"] # first two digits is position in row-col where 0, 0 is top left square on board (always facing DOWN and always length 2) + self.boat_pos = boat_pos + + self.row_length = len(self.board_rows[0]) + self.num_rows = len(self.board_rows) + self.win_condition = "43" + + + for x in boat_pos: + curr_shift_string += x + + hash = self.hash(curr_shift_string) #need to change/account for + return hash + + return 0 + + def rowsWithBoats(self): + rows_w_boats = [] + for i in range(len(self.boat_pos)): + pos_1 = int(self.boat_pos[i][0]) + pos_2 = pos_1 - 1 + if pos_1 not in rows_w_boats: + rows_w_boats.append(pos_1) + if pos_2 >= 0 and pos_2 not in rows_w_boats: # guard against -1 + rows_w_boats.append(pos_2) + return rows_w_boats + + + def rowsMoveableLeft(self): + rows_moveable_left = list(range(0, self.num_rows)) + for i in range(len(self.board_rows)): + if int(self.board_rows[i][0]) == 1: + rows_moveable_left.remove(i) + + return rows_moveable_left + + + def rowsMoveableRight(self): + rows_moveable_right = list(range(0, self.num_rows)) + for i in range(0, self.num_rows): + if int(self.board_rows[i][self.row_length - 1]) == 1: + rows_moveable_right.remove(i) + + return rows_moveable_right + + + def overlappingRows(self): + overlapping_rows = [] + # Compare as ints, not strings + b0_row = int(self.boat_pos[0][0]) + b1_row = int(self.boat_pos[1][0]) + + # Boats overlap if their occupied rows intersect + # boat occupies rows: [bottom, bottom-1] + b0_rows = {b0_row, b0_row - 1} + b1_rows = {b1_row, b1_row - 1} + + if b0_rows & b1_rows: # non-empty intersection + overlapping_rows = list(b0_rows | b1_rows) + + return overlapping_rows + + def moveRowsRight(self, rows): + stringToReturn = "" + for orig_row_i in range(0, self.num_rows): + if orig_row_i in rows: + stringToReturn += "0" + self.board_rows[orig_row_i][:self.row_length - 1] + else: + stringToReturn += self.board_rows[orig_row_i] + + return stringToReturn + + def moveRowsLeft(self,rows): + stringToReturn = "" + for orig_row_i in range(0, self.num_rows): + if orig_row_i in rows: + stringToReturn += self.board_rows[orig_row_i][1:] + "0" + else: + stringToReturn += self.board_rows[orig_row_i] + + return stringToReturn + + def returnRows(self): + stringToReturn = "" + + for orig_row in self.board_rows: + stringToReturn += orig_row + + return stringToReturn + + + def generate_moves(self, position: int): + string_rep = self.translate(self.unhash(position)) + self.board_rows = [string_rep[i:i+7] for i in range(0, 35, 7)] + # Also update boat_pos from the position + boat_str = string_rep[35:] + self.boat_pos = [boat_str[i:i+2] for i in range(0, len(boat_str), 2)] + + positions = [] + leftable_rows = self.rowsMoveableLeft() + rightable_rows = self.rowsMoveableRight() + touchable_rows = self.rowsWithBoats() + overlapping_rows = self.overlappingRows() + + # 1. Move any non-boat row left + for row in range(0, self.num_rows): + if row not in touchable_rows and row in leftable_rows: + stringToReturn = self.moveRowsLeft([row]) + for bp in self.boat_pos: + stringToReturn += bp + positions.append(self.hash(self.untranslate(stringToReturn))) + + # 2. Move any non-boat row right + for row in range(0, self.num_rows): + if row not in touchable_rows and row in rightable_rows: + stringToReturn = self.moveRowsRight([row]) + for bp in self.boat_pos: + stringToReturn += bp + positions.append(self.hash(self.untranslate(stringToReturn))) + + # 3. Move any boat left (rows move left, boat col decreases) + if overlapping_rows: + if all(row in leftable_rows for row in overlapping_rows): + stringToReturn = self.moveRowsLeft(overlapping_rows) + for bp in self.boat_pos: + col = int(bp[1]) - 1 + if col < 0: + col = self.row_length - 1 + stringToReturn += bp[0] + str(col) + positions.append(self.hash(self.untranslate(stringToReturn))) + else: + for boat_i in range(len(self.boat_pos)): + bp = self.boat_pos[boat_i] + rows = [int(bp[0]), int(bp[0]) - 1] + if all(row in leftable_rows for row in rows) and int(bp[1]) > 0: + stringToReturn = self.moveRowsLeft(rows) + for j, other_bp in enumerate(self.boat_pos): + if j == boat_i: + stringToReturn += other_bp[0] + str(int(other_bp[1]) - 1) + else: + stringToReturn += other_bp + positions.append(self.hash(self.untranslate(stringToReturn))) + + # 4. Move any boat right (rows move right, boat col increases) + if overlapping_rows: + if all(row in rightable_rows for row in overlapping_rows): + stringToReturn = self.moveRowsRight(overlapping_rows) + for bp in self.boat_pos: + col = int(bp[1]) + 1 + if col > self.row_length - 1: + col = 0 + stringToReturn += bp[0] + str(col) + positions.append(self.hash(self.untranslate(stringToReturn))) + else: + for boat_i in range(len(self.boat_pos)): + bp = self.boat_pos[boat_i] + rows = [int(bp[0]), int(bp[0]) - 1] + if all(row in rightable_rows for row in rows) and int(bp[1]) < self.row_length - 1: + stringToReturn = self.moveRowsRight(rows) + for j, other_bp in enumerate(self.boat_pos): + if j == boat_i: + stringToReturn += other_bp[0] + str(int(other_bp[1]) + 1) + else: + stringToReturn += other_bp + positions.append(self.hash(self.untranslate(stringToReturn))) + + # 5. Slide any boat up or down + for boat_i in range(len(self.boat_pos)): + bp = self.boat_pos[boat_i] + row = int(bp[0]) + col = int(bp[1]) + + # The board is translated (shifted), so col indexes correctly into board_rows + # Move down: check row+1 exists and is empty + if row < self.num_rows - 1 and self.board_rows[row + 1][col] == '0': + stringToReturn = self.returnRows() + for j, other_bp in enumerate(self.boat_pos): + if j == boat_i: + stringToReturn += str(row + 1) + str(col) + else: + stringToReturn += other_bp + positions.append(self.hash(self.untranslate(stringToReturn))) + + # Move up: top cell is at row-1, new top would be row-2 + if row > 0 and self.board_rows[row - 2][col] == '0': + stringToReturn = self.returnRows() + for j, other_bp in enumerate(self.boat_pos): + if j == boat_i: + stringToReturn += str(row - 1) + str(col) + else: + stringToReturn += other_bp + positions.append(self.hash(self.untranslate(stringToReturn))) + + return positions + + def do_move(self, position: int, move: int) -> int: + """ + Returns the resulting position of applying move to position. + """ + # If move is already a position (from generate_moves), return it directly + # Otherwise, this could be an index into the moves list + possible_moves = self.generate_moves(position) + if isinstance(move, int) and 0 <= move < len(possible_moves): + return possible_moves[move] + return move + + def primitive(self, position: int) -> Optional[Value]: + """ + Returns a Value enum which defines whether the current position is a win, loss, or non-terminal. + """ + string_rep = self.translate(self.unhash(position)) + + boats_start = 35 + if (string_rep[(self.row_length * self.num_rows):(self.row_length * self.num_rows + 2)] == self.win_condition): + return Value.Win + return None + + def CoordinateToPosition(self, x: int, y: int) -> int: + return x + y * self.row_length # board is row_length columns wide + + def to_string(self, position: int, mode: StringMode) -> str: + + string_rep = self.translate(self.unhash(position)) + waveString = string_rep[:self.row_length * self.num_rows] + boatString = string_rep[self.row_length * self.num_rows:] + + # pretend that the center of each tile is a wave or not + if mode == StringMode.AUTOGUI: + #translate the waves + waves = ['W' if char == '1' else '-' for char in waveString] + #translate the boats; need to be coordinates in fashion of coords + boat = [] + red_row = int(boatString[0]); + red_col = int(boatString[1]); + blue_row = int(boatString[2]); + blue_col = int(boatString[3]); + for i in range(0, 7): + for j in range(0, 5): + if j == red_row and i == red_col: + boat += ['R'] + elif j == blue_row and i == blue_col: + boat += ['B'] + else: + boat += ['-'] + + stringWave = "" + for string in waves: + stringWave += string + + stringBoat = "" + for string in boat: + stringBoat += string + + return "1_" + stringWave + stringBoat + elif mode == StringMode.READABLE: + + + # Build base grid from wave data + string_view = list(''.join(['~' if x == '1' else '.' for x in waveString])) + + i = 0 + color = 0 + while i + 2 <= len(boatString): + boatSlice = boatString[i:i+2] + row = int(boatSlice[0]) # bottom cell row + col = int(boatSlice[1]) # column + + bottom_idx = self.CoordinateToPosition(col, row) + top_idx = self.CoordinateToPosition(col, row - 1) + + if 0 <= bottom_idx < self.row_length * self.num_rows and 0 <= top_idx < self.row_length * self.num_rows: + string_view[bottom_idx] = self.colors[color].upper() # bottom = uppercase + string_view[top_idx] = self.colors[color].lower() # top = lowercase + + color += 1 + i += 2 + + string_view = ''.join(string_view) + return "\n".join([string_view[self.row_length*x: self.row_length*x + self.row_length] for x in range(self.num_rows)]) + + def from_string(self, strposition: str) -> int: + """ + Returns the position from a string representation of the position. + Input string is StringMode.Readable. + """ + # Ensure the game is initialized with required dimensions + if self.num_rows == 0 or self.row_length == 0: + # Initialize with variant "a" defaults + if self._variant_id in ("a", "regular"): + self.default_rows = ["1011100","1010100","1101100","1011100","1110100"] + self.board_rows = ["0101110","0101010","0011011","0101110","0111010"] + self.boat_pos = ["24", "12"] + self.row_length = len(self.board_rows[0]) + self.num_rows = len(self.board_rows) + self.win_condition = "43" + + rows = strposition.split("\n") + binary_str = "" + boat_positions = {} # color -> (row, col) of bottom cell + + for row_idx, row in enumerate(rows): + for col_idx, char in enumerate(row): + if char == '~': + binary_str += '1' + elif char == '.': + binary_str += '0' + elif char.upper() in self.colors: + binary_str += '0' # boat cell is not a wave + if char.isupper(): # uppercase = bottom cell + boat_positions[char.upper()] = (row_idx, col_idx) + else: + binary_str += '0' # fallback + + # Build boat_pos list in the same order as self.colors + boat_pos_list = [] + for color in self.colors: + if color in boat_positions: + row, col = boat_positions[color] + boat_pos_list.append(f"{row}{col}") + + # Combine binary board + boat positions, then untranslate and hash + full_string = binary_str + "".join(boat_pos_list) + return self.hash(self.untranslate(full_string)) + + + def move_to_string(self, move: int, mode: StringMode) -> str: + if mode != StringMode.Readable: + return str(move) + + # Get current board state from self.board_rows and self.boat_pos + # (already set by the most recent generate_moves call) + move_rep = self.translate(self.unhash(move)) + move_rows = [move_rep[i:i+self.row_length] for i in range(0, self.row_length * self.num_rows, self.row_length)] + move_boat_str = move_rep[self.row_length * self.num_rows:] + move_boats = [move_boat_str[i:i+2] for i in range(0, len(move_boat_str), 2)] + + curr_rows = self.board_rows + curr_boats = self.boat_pos + + # Check if any boat moved + for boat_i, (curr_bp, move_bp) in enumerate(zip(curr_boats, move_boats)): + curr_row, curr_col = int(curr_bp[0]), int(curr_bp[1]) + move_row, move_col = int(move_bp[0]), int(move_bp[1]) + color = self.colors[boat_i].lower() + + if curr_col != move_col or curr_row != move_row: + if curr_row != move_row: + direction = "down" if move_row > curr_row else "up" + return f"boat{color}-{direction}" + else: + direction = "left" if move_col < curr_col else "right" + return f"boat{color}-{direction}" + + # Otherwise a wave row moved + for row_i, (curr_row, move_row) in enumerate(zip(curr_rows, move_rows)): + if curr_row != move_row: + if move_row == curr_row[1:] + "0": + return f"row{row_i}-left" + else: + return f"row{row_i}-right" + + return str(move) # fallback + + def hash(self, strPos: str) -> int: + """ + Converts a string position to an integer hash. + """ + + # strPos format: shifts (num_rows chars) + boats (2 chars per boat) + # The boats are in decimal format (e.g. "24" for row 2, col 4), convert to ternary + + wavePosString = strPos[:self.num_rows] + boatString = strPos[self.num_rows:] + boatTernaryString = "" + + for i in range(0, len(boatString), 2): + boat = boatString[i:i+2] + boatTernaryString += self.boatTernary(boat) + + result = wavePosString + boatTernaryString + + return int(result, 3) + + def unhash(self, intPos: int) -> str: + # Ensure the game is initialized with required dimensions + if self.num_rows == 0 or len(self.boat_pos) == 0: + # Initialize with variant "a" defaults + if self._variant_id in ("a", "regular"): + self.default_rows = ["1011100","1010100","1101100","1011100","1110100"] + self.board_rows = ["0101110","0101010","0011011","0101110","0111010"] + self.boat_pos = ["24", "12"] + self.row_length = len(self.board_rows[0]) + self.num_rows = len(self.board_rows) + self.win_condition = "43" + + total_length = self.num_rows + 5 * len(self.boat_pos) # 5 shifts + 5 ternary digits per boat + strPos = self.toTernaryString(intPos).rjust(total_length, "0") + + wavePosString = strPos[:self.num_rows] + boatTernaryString = strPos[self.num_rows:] + boatString = "" + + while boatTernaryString != "": + boatString += self.boatTernaryReverse(boatTernaryString[:5]) + boatTernaryString = boatTernaryString[5:] + + return wavePosString + boatString + + def boatTernary(self, boatString: str) -> str: + # Convert 2-digit decimal boat position to fixed 5-digit ternary + # max boat position is 46 (row 4, col 6) = ternary "1201" = 4 digits, so 5 is safe + boatPos = self.toTernaryString(int(boatString)).rjust(5, "0") + return boatPos + + def boatTernaryReverse(self, boatTernaryStr: str) -> str: + # Convert 5-digit ternary back to 2-digit decimal string, zero-padded + return str(int(boatTernaryStr, 3)).rjust(2, "0") + + + def toTernaryString(self, n): + if n == 0: + return "0" + + tern_digits = [] + while n: + remainder = n % 3 + tern_digits.append(str(remainder)) + n = n // 3 + + tern_str = "".join(tern_digits[::-1]) + + return tern_str + + def translate(self, str): + #turn the shifts into a board :sob: + + + shifts = [x for x in str[:self.num_rows]] + board = "" + for row, x in enumerate(shifts): + if x == '0': + board = board + self.default_rows[row] + elif x == '1': + board += "0" + self.default_rows[row][:self.row_length - 1] + else: + board += "00" + self.default_rows[row][:self.row_length - 2] + + return board + str[self.num_rows:] + + def untranslate(self, str): + + + board_part = str[:(self.num_rows * self.row_length)] + boat_part = str[self.num_rows * self.row_length:] + + shifts = "" + for row_idx in range(self.num_rows): + row = board_part[row_idx * self.row_length : (row_idx + 1) * self.row_length] + expected_row = self.default_rows[row_idx] + + if row == expected_row: + shifts += "0" + elif row == "0" + expected_row[:self.row_length - 1]: + shifts += "1" + elif row == "00" + expected_row[:self.row_length - 2]: + shifts += "2" + + + return shifts + boat_part \ No newline at end of file From 72226e8d1d34a00f01231e604208223a8fd53b78 Mon Sep 17 00:00:00 2001 From: Sora Wongsonegoro Date: Wed, 29 Apr 2026 18:14:13 -0700 Subject: [PATCH 13/29] tried to fix GUI strings + game logic --- games/src/games/StormySeas | 2 +- games/src/games/stormyseassmall.py | 80 +++++++++++++++++++++--------- 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/games/src/games/StormySeas b/games/src/games/StormySeas index 7878965..0d726ae 160000 --- a/games/src/games/StormySeas +++ b/games/src/games/StormySeas @@ -1 +1 @@ -Subproject commit 78789657f820e91b34ffa83c7beba4b9800bc224 +Subproject commit 0d726ae254b675e098361cc166ca608bc6ac6ff6 diff --git a/games/src/games/stormyseassmall.py b/games/src/games/stormyseassmall.py index 089e8d5..2ee9c7f 100644 --- a/games/src/games/stormyseassmall.py +++ b/games/src/games/stormyseassmall.py @@ -22,9 +22,14 @@ def __init__(self, variant_id: str): self.num_rows = 0 self.win_condition = "" # Example win condition - def start(self) -> int: if self._variant_id == "a": self.default_rows = ["1011100","1010100","1101100","1011100","1110100"] + self.row_length = len(self.default_rows[0]) + self.num_rows = len(self.default_rows) + self.win_condition = "43" + + def start(self) -> int: + if self._variant_id == "a": self.board_rows = ["0101110","0101010","0011011","0101110","0111010"] # use ternary digits to represent shifts? @@ -230,9 +235,7 @@ def do_move(self, position: int, move: int) -> int: """ # If move is already a position (from generate_moves), return it directly # Otherwise, this could be an index into the moves list - possible_moves = self.generate_moves(position) - if isinstance(move, int) and 0 <= move < len(possible_moves): - return possible_moves[move] + return move def primitive(self, position: int) -> Optional[Value]: @@ -257,43 +260,71 @@ def to_string(self, position: int, mode: StringMode) -> str: waveString = string_rep[:self.row_length * self.num_rows] boatString = string_rep[self.row_length * self.num_rows:] - # Build base grid from wave data - string_view = list(''.join(['~' if x == '1' else '.' for x in waveString])) + # pretend that the center of each tile is a wave or not + if mode == StringMode.AUTOGUI: + #translate the waves + + waves = ['W' if char == '1' else '-' for char in waveString] + #translate the boats; need to be coordinates in fashion of coords + red_row = int(boatString[0]); + red_col = int(boatString[1]); + blue_row = int(boatString[2]); + blue_col = int(boatString[3]); + for i in range(0, 7): + for j in range(0, 5): + if j == red_row and i == red_col: + waves[j + i * self.row_length] = 'R' + elif j == blue_row and i == blue_col: + waves[j + i * self.row_length] = 'B' + + StringtoReturn = "1_" + "".join(waves) + print(StringtoReturn) + return StringtoReturn + + else: + - i = 0 - color = 0 - while i + 2 <= len(boatString): - boatSlice = boatString[i:i+2] - row = int(boatSlice[0]) # bottom cell row - col = int(boatSlice[1]) # column + # Build base grid from wave data + string_view = list(''.join(['w' if x == '1' else 'o' for x in waveString])) - bottom_idx = self.CoordinateToPosition(col, row) - top_idx = self.CoordinateToPosition(col, row - 1) + i = 0 + color = 0 + while i + 2 <= len(boatString): + boatSlice = boatString[i:i+2] + row = int(boatSlice[0]) # bottom cell row + col = int(boatSlice[1]) # column - if 0 <= bottom_idx < self.row_length * self.num_rows and 0 <= top_idx < self.row_length * self.num_rows: - string_view[bottom_idx] = self.colors[color].upper() # bottom = uppercase - string_view[top_idx] = self.colors[color].lower() # top = lowercase + bottom_idx = self.CoordinateToPosition(col, row) + top_idx = self.CoordinateToPosition(col, row - 1) - color += 1 - i += 2 + if 0 <= bottom_idx < self.row_length * self.num_rows and 0 <= top_idx < self.row_length * self.num_rows: + string_view[bottom_idx] = self.colors[color].upper() # bottom = uppercase + string_view[top_idx] = self.colors[color].lower() # top = lowercase - string_view = ''.join(string_view) - return "\n".join([string_view[self.row_length*x: self.row_length*x + self.row_length] for x in range(self.num_rows)]) + color += 1 + i += 2 + + string_view = ''.join(string_view) + return string_view def from_string(self, strposition: str) -> int: """ Returns the position from a string representation of the position. Input string is StringMode.Readable. """ - rows = strposition.split("\n") + expected_length = self.row_length * self.num_rows + if len(strposition) < expected_length: + raise ValueError(f"Invalid readable position length: expected at least {expected_length}, got {len(strposition)}") + + rows = [strposition[i:i+self.row_length] for i in range(0, expected_length, self.row_length)] binary_str = "" boat_positions = {} # color -> (row, col) of bottom cell for row_idx, row in enumerate(rows): for col_idx, char in enumerate(row): - if char == '~': + if char == 'w': binary_str += '1' - elif char == '.': + elif char == 'o': binary_str += '0' elif char.upper() in self.colors: binary_str += '0' # boat cell is not a wave @@ -337,6 +368,7 @@ def move_to_string(self, move: int, mode: StringMode) -> str: if curr_col != move_col or curr_row != move_row: if curr_row != move_row: direction = "down" if move_row > curr_row else "up" + return f"boat{color}-{direction}" else: direction = "left" if move_col < curr_col else "right" From c0c0886b35612dbfb3ce76343f5ccaf813365070 Mon Sep 17 00:00:00 2001 From: Sora Wongsonegoro Date: Wed, 29 Apr 2026 18:16:14 -0700 Subject: [PATCH 14/29] tried to do stuff :( --- debug_stormyseas.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 debug_stormyseas.py diff --git a/debug_stormyseas.py b/debug_stormyseas.py new file mode 100644 index 0000000..e72921b --- /dev/null +++ b/debug_stormyseas.py @@ -0,0 +1,42 @@ +import sys +sys.path[:0] = ['games/src', 'models/src', 'interfaces/src', 'server/src', 'solver/src'] +from games.stormyseassmall import StormySeas + +g = StormySeas('a') +print('row_length', g.row_length, 'num_rows', g.num_rows) + +s = 'owbwwwoowBwrwooowwRwwowowwwoowwwowo' +rows = [s[i:i+g.row_length] for i in range(0, g.num_rows * g.row_length, g.row_length)] +print('rows', rows) + +binary_str = '' +boat_positions = {} +for row_idx, row in enumerate(rows): + for col_idx, char in enumerate(row): + if char == 'w': + binary_str += '1' + elif char == 'o': + binary_str += '0' + elif char.upper() in g.colors: + binary_str += '0' + if char.isupper(): + boat_positions[char.upper()] = (row_idx, col_idx) + else: + binary_str += '0' + +print('binary_str', binary_str) +print('boat_positions', boat_positions) + +boat_pos_list = [] +for color in g.colors: + if color in boat_positions: + row, col = boat_positions[color] + boat_pos_list.append(f'{row}{col}') +print('boat_pos_list', boat_pos_list) + +full_string = binary_str + ''.join(boat_pos_list) +print('full_string', full_string) +print('len full_string', len(full_string)) +print('untranslate output', g.untranslate(full_string)) +print('hash input', g.untranslate(full_string)) +print('hash result', g.hash(g.untranslate(full_string))) From d622d075ead5f2e7a8958b79582fffc0703a09fb Mon Sep 17 00:00:00 2001 From: Manlin Zhang Date: Thu, 30 Apr 2026 22:53:05 -0700 Subject: [PATCH 15/29] added note to ignore stormyseas.py file, fixed indexing error in string for stormyseassmall.py's to_string autogui string --- games/src/games/stormyseas.py | 39 +++++++++++++++++------------- games/src/games/stormyseassmall.py | 4 +-- uv.lock | 36 ++++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 20 deletions(-) diff --git a/games/src/games/stormyseas.py b/games/src/games/stormyseas.py index 547ffa4..df33540 100644 --- a/games/src/games/stormyseas.py +++ b/games/src/games/stormyseas.py @@ -1,3 +1,7 @@ + +# THIS FUNCTION IS DEFUNCT, PLEASE LOOK AT STORMYSEASSMALL INSTEAD + + from models import Game, Value, StringMode from typing import Optional @@ -258,7 +262,7 @@ def to_string(self, position: int, mode: StringMode) -> str: # pretend that the center of each tile is a wave or not if mode == StringMode.AUTOGUI: #translate the waves - waves = ['W' if char == '1' else '-' for char in waveString] + waves = ['W' if char == '0' else '-' for char in waveString] #translate the boats; need to be coordinates in fashion of coords boat = [] red_row = int(boatString[0]); @@ -283,31 +287,32 @@ def to_string(self, position: int, mode: StringMode) -> str: stringBoat += string return "1_" + stringWave + stringBoat + elif mode == StringMode.READABLE: # Build base grid from wave data - string_view = list(''.join(['~' if x == '1' else '.' for x in waveString])) + string_view = list(''.join(['~' if x == '1' else '.' for x in waveString])) - i = 0 - color = 0 - while i + 2 <= len(boatString): - boatSlice = boatString[i:i+2] - row = int(boatSlice[0]) # bottom cell row - col = int(boatSlice[1]) # column + i = 0 + color = 0 + while i + 2 <= len(boatString): + boatSlice = boatString[i:i+2] + row = int(boatSlice[0]) # bottom cell row + col = int(boatSlice[1]) # column - bottom_idx = self.CoordinateToPosition(col, row) - top_idx = self.CoordinateToPosition(col, row - 1) + bottom_idx = self.CoordinateToPosition(col, row) + top_idx = self.CoordinateToPosition(col, row - 1) - if 0 <= bottom_idx < self.row_length * self.num_rows and 0 <= top_idx < self.row_length * self.num_rows: - string_view[bottom_idx] = self.colors[color].upper() # bottom = uppercase - string_view[top_idx] = self.colors[color].lower() # top = lowercase + if 0 <= bottom_idx < self.row_length * self.num_rows and 0 <= top_idx < self.row_length * self.num_rows: + string_view[bottom_idx] = self.colors[color].upper() # bottom = uppercase + string_view[top_idx] = self.colors[color].lower() # top = lowercase - color += 1 - i += 2 + color += 1 + i += 2 - string_view = ''.join(string_view) - return "\n".join([string_view[self.row_length*x: self.row_length*x + self.row_length] for x in range(self.num_rows)]) + string_view = ''.join(string_view) + return "\n".join([string_view[self.row_length*x: self.row_length*x + self.row_length] for x in range(self.num_rows)]) def from_string(self, strposition: str) -> int: """ diff --git a/games/src/games/stormyseassmall.py b/games/src/games/stormyseassmall.py index 2ee9c7f..ac9c5f7 100644 --- a/games/src/games/stormyseassmall.py +++ b/games/src/games/stormyseassmall.py @@ -273,9 +273,9 @@ def to_string(self, position: int, mode: StringMode) -> str: for i in range(0, 7): for j in range(0, 5): if j == red_row and i == red_col: - waves[j + i * self.row_length] = 'R' + waves[i + j * self.row_length] = 'R' elif j == blue_row and i == blue_col: - waves[j + i * self.row_length] = 'B' + waves[i + j * self.row_length] = 'B' StringtoReturn = "1_" + "".join(waves) print(StringtoReturn) diff --git a/uv.lock b/uv.lock index 47b0535..bf5618c 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" [manifest] @@ -342,6 +342,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -367,6 +395,12 @@ wheels = [ name = "server" version = "0.1.0" source = { editable = "server" } +dependencies = [ + { name = "psutil" }, +] + +[package.metadata] +requires-dist = [{ name = "psutil" }] [[package]] name = "six" From d65b07beee52fcf9b5ebb9a2a7bad37927cad8fa Mon Sep 17 00:00:00 2001 From: Manlin Zhang Date: Fri, 1 May 2026 00:57:04 -0700 Subject: [PATCH 16/29] to_string method returned to what I wrote yesterday because it works with the current setup of centers better --- games/src/games/stormyseassmall.py | 34 ++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/games/src/games/stormyseassmall.py b/games/src/games/stormyseassmall.py index ac9c5f7..01511ef 100644 --- a/games/src/games/stormyseassmall.py +++ b/games/src/games/stormyseassmall.py @@ -233,9 +233,14 @@ def do_move(self, position: int, move: int) -> int: """ Returns the resulting position of applying move to position. """ - # If move is already a position (from generate_moves), return it directly - # Otherwise, this could be an index into the moves list + # generate_moves returns a list of valid next positions + possible_moves = self.generate_moves(position) + # If move is an index into the possible_moves list, return that position + if isinstance(move, int) and 0 <= move < len(possible_moves): + return possible_moves[move] + + # Otherwise assume move is already a position hash return move def primitive(self, position: int) -> Optional[Value]: @@ -265,21 +270,32 @@ def to_string(self, position: int, mode: StringMode) -> str: #translate the waves waves = ['W' if char == '1' else '-' for char in waveString] + boat = [] #translate the boats; need to be coordinates in fashion of coords red_row = int(boatString[0]); red_col = int(boatString[1]); blue_row = int(boatString[2]); blue_col = int(boatString[3]); - for i in range(0, 7): - for j in range(0, 5): + + for j in range(0, 5): + for i in range(0, 7): if j == red_row and i == red_col: - waves[i + j * self.row_length] = 'R' + boat += ['R'] elif j == blue_row and i == blue_col: - waves[i + j * self.row_length] = 'B' + boat += ['B'] + else: + boat += ['-'] + + stringWave = "" + for string in waves: + stringWave += string + + stringBoat = "" + for string in boat: + stringBoat += string + + return "1_" + stringWave + stringBoat - StringtoReturn = "1_" + "".join(waves) - print(StringtoReturn) - return StringtoReturn else: From 32bcda8d2e85a8ae1c731974d369fd4d795f21ce Mon Sep 17 00:00:00 2001 From: Sora Wongsonegoro Date: Fri, 1 May 2026 02:34:01 -0700 Subject: [PATCH 17/29] added move buttons . kind of works but theres an offset from the board which i'm not sure how to fix --- games/src/games/stormyseassmall.py | 46 +++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/games/src/games/stormyseassmall.py b/games/src/games/stormyseassmall.py index 01511ef..8afbaf1 100644 --- a/games/src/games/stormyseassmall.py +++ b/games/src/games/stormyseassmall.py @@ -363,6 +363,49 @@ def from_string(self, strposition: str) -> int: def move_to_string(self, move: int, mode: StringMode) -> str: if mode != StringMode.Readable: + + move_rep = self.translate(self.unhash(move)) + move_rows = [move_rep[i:i+self.row_length] for i in range(0, self.row_length * self.num_rows, self.row_length)] + move_boat_str = move_rep[self.row_length * self.num_rows:] + move_boats = [move_boat_str[i:i+2] for i in range(0, len(move_boat_str), 2)] + + curr_rows = self.board_rows + curr_boats = self.boat_pos + + # Check if any boat moved + for boat_i, (curr_bp, move_bp) in enumerate(zip(curr_boats, move_boats)): + curr_row, curr_col = int(curr_bp[0]), int(curr_bp[1]) + move_row, move_col = int(move_bp[0]), int(move_bp[1]) + color = self.colors[boat_i].lower() + + if curr_col != move_col or curr_row != move_row: + if curr_row != move_row: + direction = "down" if move_row > curr_row else "up" + start = curr_row * self.row_length + curr_col + 42 + end = move_row * self.row_length + move_col + 42 + + return f"M_{start}_{end}_x" + + else: + direction = "left" if move_col < curr_col else "right" + start = curr_row * self.row_length + curr_col + 42 + end = move_row * self.row_length + move_col + 42 + + return f"M_{start}_{end}_x" + + # Otherwise a wave row moved + for row_i, (curr_row, move_row) in enumerate(zip(curr_rows, move_rows)): + if curr_row != move_row: + if move_row == curr_row[1:] + "0": + start = row_i * self.row_length + 43 + end = row_i * self.row_length + 42 + + return f"M_{start}_{end}_x" + else: + start = row_i * self.row_length + 47 + end = row_i * self.row_length + 48 + return f"M_{start}_{end}_x" + return str(move) # Get current board state from self.board_rows and self.boat_pos @@ -422,7 +465,8 @@ def hash(self, strPos: str) -> int: return int(result, 3) def unhash(self, intPos: int) -> str: - total_length = self.num_rows + 5 * len(self.boat_pos) # 5 shifts + 5 ternary digits per boat + boat_count = len(self.colors) + total_length = self.num_rows + 5 * boat_count # 5 shifts + 5 ternary digits per boat strPos = self.toTernaryString(intPos).rjust(total_length, "0") wavePosString = strPos[:self.num_rows] From 191f8bbc0fbe686a896104a818c4ae8088ad185e Mon Sep 17 00:00:00 2001 From: Manlin Zhang Date: Mon, 11 May 2026 20:36:19 -0700 Subject: [PATCH 18/29] only line changed is boat_pos, switched red and blue; feel free to change back. Currently errors when both boats are being moved to the same position --- games/src/games/stormyseassmall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games/src/games/stormyseassmall.py b/games/src/games/stormyseassmall.py index 8afbaf1..8734b41 100644 --- a/games/src/games/stormyseassmall.py +++ b/games/src/games/stormyseassmall.py @@ -34,7 +34,7 @@ def start(self) -> int: # use ternary digits to represent shifts? curr_shift_string = "11211" - boat_pos = ["24", "12"] # first two digits is position in x-y where 0, 0 is top left square on board (always facing DOWN and always length 2) + boat_pos = ["12", "24"] # first two digits is position in x-y where 0, 0 is top left square on board (always facing DOWN and always length 2) self.boat_pos = boat_pos self.row_length = len(self.board_rows[0]) From 1be9c7349724d377836c9230686a7fff4c71d184 Mon Sep 17 00:00:00 2001 From: Manlin Zhang Date: Sat, 16 May 2026 12:46:45 -0700 Subject: [PATCH 19/29] reverted to original position where red boat is closer to port --- debug_stormyseas.py | 2 +- games/src/games/game_manager.py | 2 +- games/src/games/stormyseas.py | 137 +++++--- games/src/games/stormyseassmall.py | 542 ----------------------------- 4 files changed, 89 insertions(+), 594 deletions(-) delete mode 100644 games/src/games/stormyseassmall.py diff --git a/debug_stormyseas.py b/debug_stormyseas.py index e72921b..55e4272 100644 --- a/debug_stormyseas.py +++ b/debug_stormyseas.py @@ -1,6 +1,6 @@ import sys sys.path[:0] = ['games/src', 'models/src', 'interfaces/src', 'server/src', 'solver/src'] -from games.stormyseassmall import StormySeas +from games.stormyseas import StormySeas g = StormySeas('a') print('row_length', g.row_length, 'num_rows', g.num_rows) diff --git a/games/src/games/game_manager.py b/games/src/games/game_manager.py index 3a750b5..1a45096 100644 --- a/games/src/games/game_manager.py +++ b/games/src/games/game_manager.py @@ -3,7 +3,7 @@ from .pancakes import Pancakes from .chipschallenge import ChipsChallenge from .test import Test -from .stormyseassmall import StormySeas +from .stormyseas import StormySeas from models import * game_list = { diff --git a/games/src/games/stormyseas.py b/games/src/games/stormyseas.py index df33540..8afbaf1 100644 --- a/games/src/games/stormyseas.py +++ b/games/src/games/stormyseas.py @@ -1,13 +1,9 @@ - -# THIS FUNCTION IS DEFUNCT, PLEASE LOOK AT STORMYSEASSMALL INSTEAD - - from models import Game, Value, StringMode from typing import Optional class StormySeas(Game): id = 'stormyseas' - variants = ["a", "regular"] # 'regular' maps to 'a' in the API + variants = ["a"] n_players = 1 cyclic = False colors = ["R", "B"] @@ -26,14 +22,19 @@ def __init__(self, variant_id: str): self.num_rows = 0 self.win_condition = "" # Example win condition - def start(self) -> int: - if self._variant_id in ("a", "regular"): + if self._variant_id == "a": self.default_rows = ["1011100","1010100","1101100","1011100","1110100"] + self.row_length = len(self.default_rows[0]) + self.num_rows = len(self.default_rows) + self.win_condition = "43" + + def start(self) -> int: + if self._variant_id == "a": self.board_rows = ["0101110","0101010","0011011","0101110","0111010"] # use ternary digits to represent shifts? curr_shift_string = "11211" - boat_pos = ["24", "12"] # first two digits is position in row-col where 0, 0 is top left square on board (always facing DOWN and always length 2) + boat_pos = ["24", "12"] # first two digits is position in x-y where 0, 0 is top left square on board (always facing DOWN and always length 2) self.boat_pos = boat_pos self.row_length = len(self.board_rows[0]) @@ -232,11 +233,14 @@ def do_move(self, position: int, move: int) -> int: """ Returns the resulting position of applying move to position. """ - # If move is already a position (from generate_moves), return it directly - # Otherwise, this could be an index into the moves list + # generate_moves returns a list of valid next positions possible_moves = self.generate_moves(position) + + # If move is an index into the possible_moves list, return that position if isinstance(move, int) and 0 <= move < len(possible_moves): return possible_moves[move] + + # Otherwise assume move is already a position hash return move def primitive(self, position: int) -> Optional[Value]: @@ -254,7 +258,9 @@ def CoordinateToPosition(self, x: int, y: int) -> int: return x + y * self.row_length # board is row_length columns wide def to_string(self, position: int, mode: StringMode) -> str: - + """ + Returns a string representation of the position based on the given mode. + """ string_rep = self.translate(self.unhash(position)) waveString = string_rep[:self.row_length * self.num_rows] boatString = string_rep[self.row_length * self.num_rows:] @@ -262,15 +268,17 @@ def to_string(self, position: int, mode: StringMode) -> str: # pretend that the center of each tile is a wave or not if mode == StringMode.AUTOGUI: #translate the waves - waves = ['W' if char == '0' else '-' for char in waveString] - #translate the boats; need to be coordinates in fashion of coords + + waves = ['W' if char == '1' else '-' for char in waveString] boat = [] + #translate the boats; need to be coordinates in fashion of coords red_row = int(boatString[0]); red_col = int(boatString[1]); blue_row = int(boatString[2]); blue_col = int(boatString[3]); - for i in range(0, 7): - for j in range(0, 5): + + for j in range(0, 5): + for i in range(0, 7): if j == red_row and i == red_col: boat += ['R'] elif j == blue_row and i == blue_col: @@ -287,12 +295,13 @@ def to_string(self, position: int, mode: StringMode) -> str: stringBoat += string return "1_" + stringWave + stringBoat - - elif mode == StringMode.READABLE: - - # Build base grid from wave data - string_view = list(''.join(['~' if x == '1' else '.' for x in waveString])) + + else: + + + # Build base grid from wave data + string_view = list(''.join(['w' if x == '1' else 'o' for x in waveString])) i = 0 color = 0 @@ -312,33 +321,26 @@ def to_string(self, position: int, mode: StringMode) -> str: i += 2 string_view = ''.join(string_view) - return "\n".join([string_view[self.row_length*x: self.row_length*x + self.row_length] for x in range(self.num_rows)]) + return string_view def from_string(self, strposition: str) -> int: """ Returns the position from a string representation of the position. Input string is StringMode.Readable. """ - # Ensure the game is initialized with required dimensions - if self.num_rows == 0 or self.row_length == 0: - # Initialize with variant "a" defaults - if self._variant_id in ("a", "regular"): - self.default_rows = ["1011100","1010100","1101100","1011100","1110100"] - self.board_rows = ["0101110","0101010","0011011","0101110","0111010"] - self.boat_pos = ["24", "12"] - self.row_length = len(self.board_rows[0]) - self.num_rows = len(self.board_rows) - self.win_condition = "43" - - rows = strposition.split("\n") + expected_length = self.row_length * self.num_rows + if len(strposition) < expected_length: + raise ValueError(f"Invalid readable position length: expected at least {expected_length}, got {len(strposition)}") + + rows = [strposition[i:i+self.row_length] for i in range(0, expected_length, self.row_length)] binary_str = "" boat_positions = {} # color -> (row, col) of bottom cell for row_idx, row in enumerate(rows): for col_idx, char in enumerate(row): - if char == '~': + if char == 'w': binary_str += '1' - elif char == '.': + elif char == 'o': binary_str += '0' elif char.upper() in self.colors: binary_str += '0' # boat cell is not a wave @@ -361,6 +363,49 @@ def from_string(self, strposition: str) -> int: def move_to_string(self, move: int, mode: StringMode) -> str: if mode != StringMode.Readable: + + move_rep = self.translate(self.unhash(move)) + move_rows = [move_rep[i:i+self.row_length] for i in range(0, self.row_length * self.num_rows, self.row_length)] + move_boat_str = move_rep[self.row_length * self.num_rows:] + move_boats = [move_boat_str[i:i+2] for i in range(0, len(move_boat_str), 2)] + + curr_rows = self.board_rows + curr_boats = self.boat_pos + + # Check if any boat moved + for boat_i, (curr_bp, move_bp) in enumerate(zip(curr_boats, move_boats)): + curr_row, curr_col = int(curr_bp[0]), int(curr_bp[1]) + move_row, move_col = int(move_bp[0]), int(move_bp[1]) + color = self.colors[boat_i].lower() + + if curr_col != move_col or curr_row != move_row: + if curr_row != move_row: + direction = "down" if move_row > curr_row else "up" + start = curr_row * self.row_length + curr_col + 42 + end = move_row * self.row_length + move_col + 42 + + return f"M_{start}_{end}_x" + + else: + direction = "left" if move_col < curr_col else "right" + start = curr_row * self.row_length + curr_col + 42 + end = move_row * self.row_length + move_col + 42 + + return f"M_{start}_{end}_x" + + # Otherwise a wave row moved + for row_i, (curr_row, move_row) in enumerate(zip(curr_rows, move_rows)): + if curr_row != move_row: + if move_row == curr_row[1:] + "0": + start = row_i * self.row_length + 43 + end = row_i * self.row_length + 42 + + return f"M_{start}_{end}_x" + else: + start = row_i * self.row_length + 47 + end = row_i * self.row_length + 48 + return f"M_{start}_{end}_x" + return str(move) # Get current board state from self.board_rows and self.boat_pos @@ -382,6 +427,7 @@ def move_to_string(self, move: int, mode: StringMode) -> str: if curr_col != move_col or curr_row != move_row: if curr_row != move_row: direction = "down" if move_row > curr_row else "up" + return f"boat{color}-{direction}" else: direction = "left" if move_col < curr_col else "right" @@ -402,9 +448,10 @@ def hash(self, strPos: str) -> int: Converts a string position to an integer hash. """ - # strPos format: shifts (num_rows chars) + boats (2 chars per boat) - # The boats are in decimal format (e.g. "24" for row 2, col 4), convert to ternary - + # first 8 characters of strPos is the ternary rep of waves + # rest are regular numbers, first convert these to ternary via boatTernary + # put string back together, then the final string convert into an integer via int(x, 3) + wavePosString = strPos[:self.num_rows] boatString = strPos[self.num_rows:] boatTernaryString = "" @@ -418,18 +465,8 @@ def hash(self, strPos: str) -> int: return int(result, 3) def unhash(self, intPos: int) -> str: - # Ensure the game is initialized with required dimensions - if self.num_rows == 0 or len(self.boat_pos) == 0: - # Initialize with variant "a" defaults - if self._variant_id in ("a", "regular"): - self.default_rows = ["1011100","1010100","1101100","1011100","1110100"] - self.board_rows = ["0101110","0101010","0011011","0101110","0111010"] - self.boat_pos = ["24", "12"] - self.row_length = len(self.board_rows[0]) - self.num_rows = len(self.board_rows) - self.win_condition = "43" - - total_length = self.num_rows + 5 * len(self.boat_pos) # 5 shifts + 5 ternary digits per boat + boat_count = len(self.colors) + total_length = self.num_rows + 5 * boat_count # 5 shifts + 5 ternary digits per boat strPos = self.toTernaryString(intPos).rjust(total_length, "0") wavePosString = strPos[:self.num_rows] diff --git a/games/src/games/stormyseassmall.py b/games/src/games/stormyseassmall.py deleted file mode 100644 index 8734b41..0000000 --- a/games/src/games/stormyseassmall.py +++ /dev/null @@ -1,542 +0,0 @@ -from models import Game, Value, StringMode -from typing import Optional - -class StormySeas(Game): - id = 'stormyseas' - variants = ["a"] - n_players = 1 - cyclic = False - colors = ["R", "B"] - - def __init__(self, variant_id: str): - """ - Define instance variables here (i.e. variant information) - """ - if variant_id not in StormySeas.variants: - raise ValueError("Variant not defined") - self._variant_id = variant_id - self.board_rows = [] - self.default_rows = [] - self.boat_pos = [] - self.row_length = 0 - self.num_rows = 0 - self.win_condition = "" # Example win condition - - if self._variant_id == "a": - self.default_rows = ["1011100","1010100","1101100","1011100","1110100"] - self.row_length = len(self.default_rows[0]) - self.num_rows = len(self.default_rows) - self.win_condition = "43" - - def start(self) -> int: - if self._variant_id == "a": - self.board_rows = ["0101110","0101010","0011011","0101110","0111010"] - - # use ternary digits to represent shifts? - curr_shift_string = "11211" - boat_pos = ["12", "24"] # first two digits is position in x-y where 0, 0 is top left square on board (always facing DOWN and always length 2) - self.boat_pos = boat_pos - - self.row_length = len(self.board_rows[0]) - self.num_rows = len(self.board_rows) - self.win_condition = "43" - - - for x in boat_pos: - curr_shift_string += x - - hash = self.hash(curr_shift_string) #need to change/account for - return hash - - return 0 - - def rowsWithBoats(self): - rows_w_boats = [] - for i in range(len(self.boat_pos)): - pos_1 = int(self.boat_pos[i][0]) - pos_2 = pos_1 - 1 - if pos_1 not in rows_w_boats: - rows_w_boats.append(pos_1) - if pos_2 >= 0 and pos_2 not in rows_w_boats: # guard against -1 - rows_w_boats.append(pos_2) - return rows_w_boats - - - def rowsMoveableLeft(self): - rows_moveable_left = list(range(0, self.num_rows)) - for i in range(len(self.board_rows)): - if int(self.board_rows[i][0]) == 1: - rows_moveable_left.remove(i) - - return rows_moveable_left - - - def rowsMoveableRight(self): - rows_moveable_right = list(range(0, self.num_rows)) - for i in range(0, self.num_rows): - if int(self.board_rows[i][self.row_length - 1]) == 1: - rows_moveable_right.remove(i) - - return rows_moveable_right - - - def overlappingRows(self): - overlapping_rows = [] - # Compare as ints, not strings - b0_row = int(self.boat_pos[0][0]) - b1_row = int(self.boat_pos[1][0]) - - # Boats overlap if their occupied rows intersect - # boat occupies rows: [bottom, bottom-1] - b0_rows = {b0_row, b0_row - 1} - b1_rows = {b1_row, b1_row - 1} - - if b0_rows & b1_rows: # non-empty intersection - overlapping_rows = list(b0_rows | b1_rows) - - return overlapping_rows - - def moveRowsRight(self, rows): - stringToReturn = "" - for orig_row_i in range(0, self.num_rows): - if orig_row_i in rows: - stringToReturn += "0" + self.board_rows[orig_row_i][:self.row_length - 1] - else: - stringToReturn += self.board_rows[orig_row_i] - - return stringToReturn - - def moveRowsLeft(self,rows): - stringToReturn = "" - for orig_row_i in range(0, self.num_rows): - if orig_row_i in rows: - stringToReturn += self.board_rows[orig_row_i][1:] + "0" - else: - stringToReturn += self.board_rows[orig_row_i] - - return stringToReturn - - def returnRows(self): - stringToReturn = "" - - for orig_row in self.board_rows: - stringToReturn += orig_row - - return stringToReturn - - - def generate_moves(self, position: int): - string_rep = self.translate(self.unhash(position)) - self.board_rows = [string_rep[i:i+7] for i in range(0, 35, 7)] - # Also update boat_pos from the position - boat_str = string_rep[35:] - self.boat_pos = [boat_str[i:i+2] for i in range(0, len(boat_str), 2)] - - positions = [] - leftable_rows = self.rowsMoveableLeft() - rightable_rows = self.rowsMoveableRight() - touchable_rows = self.rowsWithBoats() - overlapping_rows = self.overlappingRows() - - # 1. Move any non-boat row left - for row in range(0, self.num_rows): - if row not in touchable_rows and row in leftable_rows: - stringToReturn = self.moveRowsLeft([row]) - for bp in self.boat_pos: - stringToReturn += bp - positions.append(self.hash(self.untranslate(stringToReturn))) - - # 2. Move any non-boat row right - for row in range(0, self.num_rows): - if row not in touchable_rows and row in rightable_rows: - stringToReturn = self.moveRowsRight([row]) - for bp in self.boat_pos: - stringToReturn += bp - positions.append(self.hash(self.untranslate(stringToReturn))) - - # 3. Move any boat left (rows move left, boat col decreases) - if overlapping_rows: - if all(row in leftable_rows for row in overlapping_rows): - stringToReturn = self.moveRowsLeft(overlapping_rows) - for bp in self.boat_pos: - col = int(bp[1]) - 1 - if col < 0: - col = self.row_length - 1 - stringToReturn += bp[0] + str(col) - positions.append(self.hash(self.untranslate(stringToReturn))) - else: - for boat_i in range(len(self.boat_pos)): - bp = self.boat_pos[boat_i] - rows = [int(bp[0]), int(bp[0]) - 1] - if all(row in leftable_rows for row in rows) and int(bp[1]) > 0: - stringToReturn = self.moveRowsLeft(rows) - for j, other_bp in enumerate(self.boat_pos): - if j == boat_i: - stringToReturn += other_bp[0] + str(int(other_bp[1]) - 1) - else: - stringToReturn += other_bp - positions.append(self.hash(self.untranslate(stringToReturn))) - - # 4. Move any boat right (rows move right, boat col increases) - if overlapping_rows: - if all(row in rightable_rows for row in overlapping_rows): - stringToReturn = self.moveRowsRight(overlapping_rows) - for bp in self.boat_pos: - col = int(bp[1]) + 1 - if col > self.row_length - 1: - col = 0 - stringToReturn += bp[0] + str(col) - positions.append(self.hash(self.untranslate(stringToReturn))) - else: - for boat_i in range(len(self.boat_pos)): - bp = self.boat_pos[boat_i] - rows = [int(bp[0]), int(bp[0]) - 1] - if all(row in rightable_rows for row in rows) and int(bp[1]) < self.row_length - 1: - stringToReturn = self.moveRowsRight(rows) - for j, other_bp in enumerate(self.boat_pos): - if j == boat_i: - stringToReturn += other_bp[0] + str(int(other_bp[1]) + 1) - else: - stringToReturn += other_bp - positions.append(self.hash(self.untranslate(stringToReturn))) - - # 5. Slide any boat up or down - for boat_i in range(len(self.boat_pos)): - bp = self.boat_pos[boat_i] - row = int(bp[0]) - col = int(bp[1]) - - # The board is translated (shifted), so col indexes correctly into board_rows - # Move down: check row+1 exists and is empty - if row < self.num_rows - 1 and self.board_rows[row + 1][col] == '0': - stringToReturn = self.returnRows() - for j, other_bp in enumerate(self.boat_pos): - if j == boat_i: - stringToReturn += str(row + 1) + str(col) - else: - stringToReturn += other_bp - positions.append(self.hash(self.untranslate(stringToReturn))) - - # Move up: top cell is at row-1, new top would be row-2 - if row > 0 and self.board_rows[row - 2][col] == '0': - stringToReturn = self.returnRows() - for j, other_bp in enumerate(self.boat_pos): - if j == boat_i: - stringToReturn += str(row - 1) + str(col) - else: - stringToReturn += other_bp - positions.append(self.hash(self.untranslate(stringToReturn))) - - return positions - - def do_move(self, position: int, move: int) -> int: - """ - Returns the resulting position of applying move to position. - """ - # generate_moves returns a list of valid next positions - possible_moves = self.generate_moves(position) - - # If move is an index into the possible_moves list, return that position - if isinstance(move, int) and 0 <= move < len(possible_moves): - return possible_moves[move] - - # Otherwise assume move is already a position hash - return move - - def primitive(self, position: int) -> Optional[Value]: - """ - Returns a Value enum which defines whether the current position is a win, loss, or non-terminal. - """ - string_rep = self.translate(self.unhash(position)) - - boats_start = 35 - if (string_rep[(self.row_length * self.num_rows):(self.row_length * self.num_rows + 2)] == self.win_condition): - return Value.Win - return None - - def CoordinateToPosition(self, x: int, y: int) -> int: - return x + y * self.row_length # board is row_length columns wide - - def to_string(self, position: int, mode: StringMode) -> str: - """ - Returns a string representation of the position based on the given mode. - """ - string_rep = self.translate(self.unhash(position)) - waveString = string_rep[:self.row_length * self.num_rows] - boatString = string_rep[self.row_length * self.num_rows:] - - # pretend that the center of each tile is a wave or not - if mode == StringMode.AUTOGUI: - #translate the waves - - waves = ['W' if char == '1' else '-' for char in waveString] - boat = [] - #translate the boats; need to be coordinates in fashion of coords - red_row = int(boatString[0]); - red_col = int(boatString[1]); - blue_row = int(boatString[2]); - blue_col = int(boatString[3]); - - for j in range(0, 5): - for i in range(0, 7): - if j == red_row and i == red_col: - boat += ['R'] - elif j == blue_row and i == blue_col: - boat += ['B'] - else: - boat += ['-'] - - stringWave = "" - for string in waves: - stringWave += string - - stringBoat = "" - for string in boat: - stringBoat += string - - return "1_" + stringWave + stringBoat - - - else: - - - # Build base grid from wave data - string_view = list(''.join(['w' if x == '1' else 'o' for x in waveString])) - - i = 0 - color = 0 - while i + 2 <= len(boatString): - boatSlice = boatString[i:i+2] - row = int(boatSlice[0]) # bottom cell row - col = int(boatSlice[1]) # column - - bottom_idx = self.CoordinateToPosition(col, row) - top_idx = self.CoordinateToPosition(col, row - 1) - - if 0 <= bottom_idx < self.row_length * self.num_rows and 0 <= top_idx < self.row_length * self.num_rows: - string_view[bottom_idx] = self.colors[color].upper() # bottom = uppercase - string_view[top_idx] = self.colors[color].lower() # top = lowercase - - color += 1 - i += 2 - - string_view = ''.join(string_view) - return string_view - - def from_string(self, strposition: str) -> int: - """ - Returns the position from a string representation of the position. - Input string is StringMode.Readable. - """ - expected_length = self.row_length * self.num_rows - if len(strposition) < expected_length: - raise ValueError(f"Invalid readable position length: expected at least {expected_length}, got {len(strposition)}") - - rows = [strposition[i:i+self.row_length] for i in range(0, expected_length, self.row_length)] - binary_str = "" - boat_positions = {} # color -> (row, col) of bottom cell - - for row_idx, row in enumerate(rows): - for col_idx, char in enumerate(row): - if char == 'w': - binary_str += '1' - elif char == 'o': - binary_str += '0' - elif char.upper() in self.colors: - binary_str += '0' # boat cell is not a wave - if char.isupper(): # uppercase = bottom cell - boat_positions[char.upper()] = (row_idx, col_idx) - else: - binary_str += '0' # fallback - - # Build boat_pos list in the same order as self.colors - boat_pos_list = [] - for color in self.colors: - if color in boat_positions: - row, col = boat_positions[color] - boat_pos_list.append(f"{row}{col}") - - # Combine binary board + boat positions, then untranslate and hash - full_string = binary_str + "".join(boat_pos_list) - return self.hash(self.untranslate(full_string)) - - - def move_to_string(self, move: int, mode: StringMode) -> str: - if mode != StringMode.Readable: - - move_rep = self.translate(self.unhash(move)) - move_rows = [move_rep[i:i+self.row_length] for i in range(0, self.row_length * self.num_rows, self.row_length)] - move_boat_str = move_rep[self.row_length * self.num_rows:] - move_boats = [move_boat_str[i:i+2] for i in range(0, len(move_boat_str), 2)] - - curr_rows = self.board_rows - curr_boats = self.boat_pos - - # Check if any boat moved - for boat_i, (curr_bp, move_bp) in enumerate(zip(curr_boats, move_boats)): - curr_row, curr_col = int(curr_bp[0]), int(curr_bp[1]) - move_row, move_col = int(move_bp[0]), int(move_bp[1]) - color = self.colors[boat_i].lower() - - if curr_col != move_col or curr_row != move_row: - if curr_row != move_row: - direction = "down" if move_row > curr_row else "up" - start = curr_row * self.row_length + curr_col + 42 - end = move_row * self.row_length + move_col + 42 - - return f"M_{start}_{end}_x" - - else: - direction = "left" if move_col < curr_col else "right" - start = curr_row * self.row_length + curr_col + 42 - end = move_row * self.row_length + move_col + 42 - - return f"M_{start}_{end}_x" - - # Otherwise a wave row moved - for row_i, (curr_row, move_row) in enumerate(zip(curr_rows, move_rows)): - if curr_row != move_row: - if move_row == curr_row[1:] + "0": - start = row_i * self.row_length + 43 - end = row_i * self.row_length + 42 - - return f"M_{start}_{end}_x" - else: - start = row_i * self.row_length + 47 - end = row_i * self.row_length + 48 - return f"M_{start}_{end}_x" - - return str(move) - - # Get current board state from self.board_rows and self.boat_pos - # (already set by the most recent generate_moves call) - move_rep = self.translate(self.unhash(move)) - move_rows = [move_rep[i:i+self.row_length] for i in range(0, self.row_length * self.num_rows, self.row_length)] - move_boat_str = move_rep[self.row_length * self.num_rows:] - move_boats = [move_boat_str[i:i+2] for i in range(0, len(move_boat_str), 2)] - - curr_rows = self.board_rows - curr_boats = self.boat_pos - - # Check if any boat moved - for boat_i, (curr_bp, move_bp) in enumerate(zip(curr_boats, move_boats)): - curr_row, curr_col = int(curr_bp[0]), int(curr_bp[1]) - move_row, move_col = int(move_bp[0]), int(move_bp[1]) - color = self.colors[boat_i].lower() - - if curr_col != move_col or curr_row != move_row: - if curr_row != move_row: - direction = "down" if move_row > curr_row else "up" - - return f"boat{color}-{direction}" - else: - direction = "left" if move_col < curr_col else "right" - return f"boat{color}-{direction}" - - # Otherwise a wave row moved - for row_i, (curr_row, move_row) in enumerate(zip(curr_rows, move_rows)): - if curr_row != move_row: - if move_row == curr_row[1:] + "0": - return f"row{row_i}-left" - else: - return f"row{row_i}-right" - - return str(move) # fallback - - def hash(self, strPos: str) -> int: - """ - Converts a string position to an integer hash. - """ - - # first 8 characters of strPos is the ternary rep of waves - # rest are regular numbers, first convert these to ternary via boatTernary - # put string back together, then the final string convert into an integer via int(x, 3) - - wavePosString = strPos[:self.num_rows] - boatString = strPos[self.num_rows:] - boatTernaryString = "" - - for i in range(0, len(boatString), 2): - boat = boatString[i:i+2] - boatTernaryString += self.boatTernary(boat) - - result = wavePosString + boatTernaryString - - return int(result, 3) - - def unhash(self, intPos: int) -> str: - boat_count = len(self.colors) - total_length = self.num_rows + 5 * boat_count # 5 shifts + 5 ternary digits per boat - strPos = self.toTernaryString(intPos).rjust(total_length, "0") - - wavePosString = strPos[:self.num_rows] - boatTernaryString = strPos[self.num_rows:] - boatString = "" - - while boatTernaryString != "": - boatString += self.boatTernaryReverse(boatTernaryString[:5]) - boatTernaryString = boatTernaryString[5:] - - return wavePosString + boatString - - def boatTernary(self, boatString: str) -> str: - # Convert 2-digit decimal boat position to fixed 5-digit ternary - # max boat position is 46 (row 4, col 6) = ternary "1201" = 4 digits, so 5 is safe - boatPos = self.toTernaryString(int(boatString)).rjust(5, "0") - return boatPos - - def boatTernaryReverse(self, boatTernaryStr: str) -> str: - # Convert 5-digit ternary back to 2-digit decimal string, zero-padded - return str(int(boatTernaryStr, 3)).rjust(2, "0") - - - def toTernaryString(self, n): - if n == 0: - return "0" - - tern_digits = [] - while n: - remainder = n % 3 - tern_digits.append(str(remainder)) - n = n // 3 - - tern_str = "".join(tern_digits[::-1]) - - return tern_str - - def translate(self, str): - #turn the shifts into a board :sob: - - - shifts = [x for x in str[:self.num_rows]] - board = "" - for row, x in enumerate(shifts): - if x == '0': - board = board + self.default_rows[row] - elif x == '1': - board += "0" + self.default_rows[row][:self.row_length - 1] - else: - board += "00" + self.default_rows[row][:self.row_length - 2] - - return board + str[self.num_rows:] - - def untranslate(self, str): - - - board_part = str[:(self.num_rows * self.row_length)] - boat_part = str[self.num_rows * self.row_length:] - - shifts = "" - for row_idx in range(self.num_rows): - row = board_part[row_idx * self.row_length : (row_idx + 1) * self.row_length] - expected_row = self.default_rows[row_idx] - - if row == expected_row: - shifts += "0" - elif row == "0" + expected_row[:self.row_length - 1]: - shifts += "1" - elif row == "00" + expected_row[:self.row_length - 2]: - shifts += "2" - - - return shifts + boat_part \ No newline at end of file From cc24ad2210b108ae739cb7e4b728ae196121472d Mon Sep 17 00:00:00 2001 From: Manlin Zhang Date: Tue, 19 May 2026 15:12:47 -0700 Subject: [PATCH 20/29] removed the debug stormyseas file as requested by Abraham --- debug_stormyseas.py | 42 -------------------------------------- games/src/games/StormySeas | 1 - 2 files changed, 43 deletions(-) delete mode 100644 debug_stormyseas.py delete mode 160000 games/src/games/StormySeas diff --git a/debug_stormyseas.py b/debug_stormyseas.py deleted file mode 100644 index 55e4272..0000000 --- a/debug_stormyseas.py +++ /dev/null @@ -1,42 +0,0 @@ -import sys -sys.path[:0] = ['games/src', 'models/src', 'interfaces/src', 'server/src', 'solver/src'] -from games.stormyseas import StormySeas - -g = StormySeas('a') -print('row_length', g.row_length, 'num_rows', g.num_rows) - -s = 'owbwwwoowBwrwooowwRwwowowwwoowwwowo' -rows = [s[i:i+g.row_length] for i in range(0, g.num_rows * g.row_length, g.row_length)] -print('rows', rows) - -binary_str = '' -boat_positions = {} -for row_idx, row in enumerate(rows): - for col_idx, char in enumerate(row): - if char == 'w': - binary_str += '1' - elif char == 'o': - binary_str += '0' - elif char.upper() in g.colors: - binary_str += '0' - if char.isupper(): - boat_positions[char.upper()] = (row_idx, col_idx) - else: - binary_str += '0' - -print('binary_str', binary_str) -print('boat_positions', boat_positions) - -boat_pos_list = [] -for color in g.colors: - if color in boat_positions: - row, col = boat_positions[color] - boat_pos_list.append(f'{row}{col}') -print('boat_pos_list', boat_pos_list) - -full_string = binary_str + ''.join(boat_pos_list) -print('full_string', full_string) -print('len full_string', len(full_string)) -print('untranslate output', g.untranslate(full_string)) -print('hash input', g.untranslate(full_string)) -print('hash result', g.hash(g.untranslate(full_string))) diff --git a/games/src/games/StormySeas b/games/src/games/StormySeas deleted file mode 160000 index 0d726ae..0000000 --- a/games/src/games/StormySeas +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0d726ae254b675e098361cc166ca608bc6ac6ff6 From a8c0329947ed17339d41f9301a8b96e935c9a23b Mon Sep 17 00:00:00 2001 From: Abraham Hsu Date: Wed, 20 May 2026 13:01:20 -0700 Subject: [PATCH 21/29] Alphabetical order --- games/src/games/game_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/games/src/games/game_manager.py b/games/src/games/game_manager.py index e66ab13..36faa9d 100644 --- a/games/src/games/game_manager.py +++ b/games/src/games/game_manager.py @@ -9,8 +9,8 @@ from .snakestale import Snakestale from .sokobaniq import SokobanIQ from .sokobanlarge import SokobanLarge -from .test import Test from .stormyseas import StormySeas +from .test import Test from models import * game_list = { @@ -25,8 +25,9 @@ "snakestale": Snakestale, "sokobaniq": SokobanIQ, "sokobanlarge": SokobanLarge, + "stormyseas": StormySeas, "test": Test, - "stormyseas": StormySeas + } def validate(game_id: str, variant_id: str) -> bool: From a74019b708c62842ae72c9104318877a56af6a79 Mon Sep 17 00:00:00 2001 From: Abraham Hsu Date: Wed, 20 May 2026 13:07:50 -0700 Subject: [PATCH 22/29] Added EOF character --- games/src/games/stormyseas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games/src/games/stormyseas.py b/games/src/games/stormyseas.py index 8afbaf1..e86ee69 100644 --- a/games/src/games/stormyseas.py +++ b/games/src/games/stormyseas.py @@ -539,4 +539,4 @@ def untranslate(self, str): shifts += "2" - return shifts + boat_part \ No newline at end of file + return shifts + boat_part From 21177340ff76130a5c2a447b391f9c631f5ae63d Mon Sep 17 00:00:00 2001 From: Abraham Hsu Date: Wed, 20 May 2026 13:08:48 -0700 Subject: [PATCH 23/29] Removed unnecessary character --- games/src/games/game_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/games/src/games/game_manager.py b/games/src/games/game_manager.py index 36faa9d..050d790 100644 --- a/games/src/games/game_manager.py +++ b/games/src/games/game_manager.py @@ -27,7 +27,6 @@ "sokobanlarge": SokobanLarge, "stormyseas": StormySeas, "test": Test, - } def validate(game_id: str, variant_id: str) -> bool: From e0f05a6b7626c5f26912236a04f63ae4a48d9854 Mon Sep 17 00:00:00 2001 From: Manlin Zhang Date: Sun, 24 May 2026 15:04:52 -0700 Subject: [PATCH 24/29] the arrows are now centered properly for the waves and boats. only issue remaining is that the win condition automatically occurs at 1 move left --- games/src/games/stormyseas.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/games/src/games/stormyseas.py b/games/src/games/stormyseas.py index e86ee69..a2d29ab 100644 --- a/games/src/games/stormyseas.py +++ b/games/src/games/stormyseas.py @@ -381,29 +381,29 @@ def move_to_string(self, move: int, mode: StringMode) -> str: if curr_col != move_col or curr_row != move_row: if curr_row != move_row: direction = "down" if move_row > curr_row else "up" - start = curr_row * self.row_length + curr_col + 42 - end = move_row * self.row_length + move_col + 42 + start = curr_row * self.row_length + curr_col + 35 + end = move_row * self.row_length + move_col + 35 return f"M_{start}_{end}_x" else: direction = "left" if move_col < curr_col else "right" - start = curr_row * self.row_length + curr_col + 42 - end = move_row * self.row_length + move_col + 42 + start = curr_row * self.row_length + curr_col + 35 + end = move_row * self.row_length + move_col + 35 return f"M_{start}_{end}_x" # Otherwise a wave row moved for row_i, (curr_row, move_row) in enumerate(zip(curr_rows, move_rows)): if curr_row != move_row: - if move_row == curr_row[1:] + "0": - start = row_i * self.row_length + 43 - end = row_i * self.row_length + 42 + if move_row == curr_row[1:] + "0": # left arrow + start = row_i * self.row_length + 1 + end = row_i * self.row_length return f"M_{start}_{end}_x" - else: - start = row_i * self.row_length + 47 - end = row_i * self.row_length + 48 + else: # right arrow + start = row_i * self.row_length + 5 + end = row_i * self.row_length + 6 return f"M_{start}_{end}_x" return str(move) From 734a92834065b4eff9a197a20b7f9612f37a5b01 Mon Sep 17 00:00:00 2001 From: Manlin Zhang Date: Sun, 24 May 2026 15:56:12 -0700 Subject: [PATCH 25/29] fixed winstate to hit one move earlier, so the GUI says puzzle is solved at the right time --- games/src/games/stormyseas.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/games/src/games/stormyseas.py b/games/src/games/stormyseas.py index a2d29ab..07a3a36 100644 --- a/games/src/games/stormyseas.py +++ b/games/src/games/stormyseas.py @@ -21,6 +21,7 @@ def __init__(self, variant_id: str): self.row_length = 0 self.num_rows = 0 self.win_condition = "" # Example win condition + self.winState = False if self._variant_id == "a": self.default_rows = ["1011100","1010100","1101100","1011100","1110100"] @@ -138,6 +139,9 @@ def generate_moves(self, position: int): touchable_rows = self.rowsWithBoats() overlapping_rows = self.overlappingRows() + if (string_rep[(self.row_length * self.num_rows):(self.row_length * self.num_rows + 2)] == self.win_condition): + positions.append(-1) + # 1. Move any non-boat row left for row in range(0, self.num_rows): if row not in touchable_rows and row in leftable_rows: @@ -240,6 +244,11 @@ def do_move(self, position: int, move: int) -> int: if isinstance(move, int) and 0 <= move < len(possible_moves): return possible_moves[move] + self.winState = False + if (move == -1): + self.winState = True + return -1 + # Otherwise assume move is already a position hash return move @@ -250,7 +259,7 @@ def primitive(self, position: int) -> Optional[Value]: string_rep = self.translate(self.unhash(position)) boats_start = 35 - if (string_rep[(self.row_length * self.num_rows):(self.row_length * self.num_rows + 2)] == self.win_condition): + if (self.winState == True): return Value.Win return None @@ -272,10 +281,10 @@ def to_string(self, position: int, mode: StringMode) -> str: waves = ['W' if char == '1' else '-' for char in waveString] boat = [] #translate the boats; need to be coordinates in fashion of coords - red_row = int(boatString[0]); - red_col = int(boatString[1]); - blue_row = int(boatString[2]); - blue_col = int(boatString[3]); + red_row = int(boatString[0]) + red_col = int(boatString[1]) + blue_row = int(boatString[2]) + blue_col = int(boatString[3]) for j in range(0, 5): for i in range(0, 7): From 0bfbc5961300da2afd57214eb00acc7aadf8bdf3 Mon Sep 17 00:00:00 2001 From: SoraWongsonegoro Date: Mon, 25 May 2026 16:54:16 -0700 Subject: [PATCH 26/29] fixed memory error issue via correcting hash function --- games/src/games/stormyseas.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/games/src/games/stormyseas.py b/games/src/games/stormyseas.py index 07a3a36..d51a0aa 100644 --- a/games/src/games/stormyseas.py +++ b/games/src/games/stormyseas.py @@ -139,9 +139,6 @@ def generate_moves(self, position: int): touchable_rows = self.rowsWithBoats() overlapping_rows = self.overlappingRows() - if (string_rep[(self.row_length * self.num_rows):(self.row_length * self.num_rows + 2)] == self.win_condition): - positions.append(-1) - # 1. Move any non-boat row left for row in range(0, self.num_rows): if row not in touchable_rows and row in leftable_rows: @@ -244,11 +241,6 @@ def do_move(self, position: int, move: int) -> int: if isinstance(move, int) and 0 <= move < len(possible_moves): return possible_moves[move] - self.winState = False - if (move == -1): - self.winState = True - return -1 - # Otherwise assume move is already a position hash return move @@ -259,8 +251,8 @@ def primitive(self, position: int) -> Optional[Value]: string_rep = self.translate(self.unhash(position)) boats_start = 35 - if (self.winState == True): - return Value.Win + if (string_rep[(self.row_length * self.num_rows):(self.row_length * self.num_rows + 2)] == self.win_condition): + return Value.Win return None def CoordinateToPosition(self, x: int, y: int) -> int: @@ -476,7 +468,23 @@ def hash(self, strPos: str) -> int: def unhash(self, intPos: int) -> str: boat_count = len(self.colors) total_length = self.num_rows + 5 * boat_count # 5 shifts + 5 ternary digits per boat - strPos = self.toTernaryString(intPos).rjust(total_length, "0") + # Ensure we have a plain Python int and that it fits in the expected + # number of ternary digits. If the value is out-of-range, raise a + # clear error instead of allowing `toTernaryString` to allocate an + # extremely large list and trigger MemoryError. + try: + int_val = int(intPos) + except Exception: + raise ValueError(f"unhash: position must be an integer-like value, got: {type(intPos)}") + + if int_val < 0: + raise ValueError(f"unhash: negative positions are invalid: {int_val}") + + max_value = 3 ** total_length - 1 + if int_val > max_value: + raise ValueError(f"unhash: position {int_val} exceeds max representable value {max_value} for total_length {total_length}") + + strPos = self.toTernaryString(int_val).rjust(total_length, "0") wavePosString = strPos[:self.num_rows] boatTernaryString = strPos[self.num_rows:] From 671827bcb216c2bacd0a2540e7ba301446b992b0 Mon Sep 17 00:00:00 2001 From: Manlin Zhang Date: Tue, 26 May 2026 16:37:16 -0700 Subject: [PATCH 27/29] the overlapping issue is fixed on gui, but solving for positions is returning only 39? --- games/src/games/stormyseas.py | 39 +++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/games/src/games/stormyseas.py b/games/src/games/stormyseas.py index d51a0aa..934b74e 100644 --- a/games/src/games/stormyseas.py +++ b/games/src/games/stormyseas.py @@ -206,27 +206,36 @@ def generate_moves(self, position: int): bp = self.boat_pos[boat_i] row = int(bp[0]) col = int(bp[1]) + boat_other = self.boat_pos[boat_i - 1] # hardcoded for 2 boats + row_other = int(boat_other[0]) + col_other = int(boat_other[1]) # The board is translated (shifted), so col indexes correctly into board_rows # Move down: check row+1 exists and is empty if row < self.num_rows - 1 and self.board_rows[row + 1][col] == '0': - stringToReturn = self.returnRows() - for j, other_bp in enumerate(self.boat_pos): - if j == boat_i: - stringToReturn += str(row + 1) + str(col) - else: - stringToReturn += other_bp - positions.append(self.hash(self.untranslate(stringToReturn))) + if row + 1 == row_other -1 and col == col_other: + pass + else: + stringToReturn = self.returnRows() + for j, other_bp in enumerate(self.boat_pos): + if j == boat_i: + stringToReturn += str(row + 1) + str(col) + else: + stringToReturn += other_bp + positions.append(self.hash(self.untranslate(stringToReturn))) # Move up: top cell is at row-1, new top would be row-2 - if row > 0 and self.board_rows[row - 2][col] == '0': - stringToReturn = self.returnRows() - for j, other_bp in enumerate(self.boat_pos): - if j == boat_i: - stringToReturn += str(row - 1) + str(col) - else: - stringToReturn += other_bp - positions.append(self.hash(self.untranslate(stringToReturn))) + if row > 1 and self.board_rows[row - 2][col] == '0': + if row - 1 == row_other + 1 and col == col_other: #check it's not adjacent + pass + else: + stringToReturn = self.returnRows() + for j, other_bp in enumerate(self.boat_pos): + if j == boat_i: + stringToReturn += str(row - 1) + str(col) + else: + stringToReturn += other_bp + positions.append(self.hash(self.untranslate(stringToReturn))) return positions From 123f17a39047b451fb01e4f03d16506cf7bcf2e1 Mon Sep 17 00:00:00 2001 From: Manlin Zhang Date: Mon, 1 Jun 2026 13:16:51 -0700 Subject: [PATCH 28/29] arrows implemented for both boats and rows, can move either where possible --- games/src/games/stormyseas.py | 157 ++++++++++++++++++++++++---------- 1 file changed, 111 insertions(+), 46 deletions(-) diff --git a/games/src/games/stormyseas.py b/games/src/games/stormyseas.py index 934b74e..f7ce4b0 100644 --- a/games/src/games/stormyseas.py +++ b/games/src/games/stormyseas.py @@ -46,7 +46,7 @@ def start(self) -> int: for x in boat_pos: curr_shift_string += x - hash = self.hash(curr_shift_string) #need to change/account for + hash = self.hash(curr_shift_string + '7') #we're using '7' as our placeholder indicator value return hash return 0 @@ -127,10 +127,12 @@ def returnRows(self): def generate_moves(self, position: int): + # update w/last commit, to make sure arrows are in every row as intended: attach 'indicator' at end of string that is the index of each row that can be moved, or '8' if attributed to red boat, or '9' if attributed to blue boat, if a boat is just being moved it doesn't matter, put '7' as a placeholder string_rep = self.translate(self.unhash(position)) self.board_rows = [string_rep[i:i+7] for i in range(0, 35, 7)] # Also update boat_pos from the position - boat_str = string_rep[35:] + boat_str = string_rep[35:39] + self.boat_pos = [boat_str[i:i+2] for i in range(0, len(boat_str), 2)] positions = [] @@ -145,6 +147,8 @@ def generate_moves(self, position: int): stringToReturn = self.moveRowsLeft([row]) for bp in self.boat_pos: stringToReturn += bp + #append special indicator of index of row being moved; redundant but for consistency + stringToReturn += str(row) positions.append(self.hash(self.untranslate(stringToReturn))) # 2. Move any non-boat row right @@ -153,9 +157,13 @@ def generate_moves(self, position: int): stringToReturn = self.moveRowsRight([row]) for bp in self.boat_pos: stringToReturn += bp + #append special indicator of index of row being moved; redundant but for consistency + stringToReturn += str(row) positions.append(self.hash(self.untranslate(stringToReturn))) # 3. Move any boat left (rows move left, boat col decreases) + + # this section will append positions assuming the two boats happen to overlap (so three rows are moved) if overlapping_rows: if all(row in leftable_rows for row in overlapping_rows): stringToReturn = self.moveRowsLeft(overlapping_rows) @@ -164,7 +172,14 @@ def generate_moves(self, position: int): if col < 0: col = self.row_length - 1 stringToReturn += bp[0] + str(col) - positions.append(self.hash(self.untranslate(stringToReturn))) + # append special indicator of index of all rows being touched + for row in overlapping_rows: + positions.append(self.hash(self.untranslate(stringToReturn + str(row)))) + # append special indicator for both boats; '8' = red, '9' = blue + positions.append(self.hash(self.untranslate(stringToReturn + '8'))) + positions.append(self.hash(self.untranslate(stringToReturn + '9'))) + + # this section assumes boats are not overlapping (so two rows are being moved) else: for boat_i in range(len(self.boat_pos)): bp = self.boat_pos[boat_i] @@ -176,7 +191,13 @@ def generate_moves(self, position: int): stringToReturn += other_bp[0] + str(int(other_bp[1]) - 1) else: stringToReturn += other_bp - positions.append(self.hash(self.untranslate(stringToReturn))) + #append special indicator for both rows being touched + for row in rows: + positions.append(self.hash(self.untranslate(stringToReturn + str(row)))) + + #append special indicator for the boat being touched + boat_touched = '8' if boat_i == 0 else '9' + positions.append(self.hash(self.untranslate(stringToReturn + boat_touched))) # 4. Move any boat right (rows move right, boat col increases) if overlapping_rows: @@ -187,7 +208,13 @@ def generate_moves(self, position: int): if col > self.row_length - 1: col = 0 stringToReturn += bp[0] + str(col) - positions.append(self.hash(self.untranslate(stringToReturn))) + + # append special indicator of index of all rows being touched + for row in overlapping_rows: + positions.append(self.hash(self.untranslate(stringToReturn + str(row)))) + # append special indicator for both boats; '8' = red, '9' = blue + positions.append(self.hash(self.untranslate(stringToReturn + '8'))) + positions.append(self.hash(self.untranslate(stringToReturn + '9'))) else: for boat_i in range(len(self.boat_pos)): bp = self.boat_pos[boat_i] @@ -199,7 +226,13 @@ def generate_moves(self, position: int): stringToReturn += other_bp[0] + str(int(other_bp[1]) + 1) else: stringToReturn += other_bp - positions.append(self.hash(self.untranslate(stringToReturn))) + #append special indicator for both rows being touched + for row in rows: + positions.append(self.hash(self.untranslate(stringToReturn + str(row)))) + + #append special indicator for the boat being touched + boat_touched = '8' if boat_i == 0 else '9' + positions.append(self.hash(self.untranslate(stringToReturn + boat_touched))) # 5. Slide any boat up or down for boat_i in range(len(self.boat_pos)): @@ -222,7 +255,7 @@ def generate_moves(self, position: int): stringToReturn += str(row + 1) + str(col) else: stringToReturn += other_bp - positions.append(self.hash(self.untranslate(stringToReturn))) + positions.append(self.hash(self.untranslate(stringToReturn + '7'))) # Move up: top cell is at row-1, new top would be row-2 if row > 1 and self.board_rows[row - 2][col] == '0': @@ -235,7 +268,7 @@ def generate_moves(self, position: int): stringToReturn += str(row - 1) + str(col) else: stringToReturn += other_bp - positions.append(self.hash(self.untranslate(stringToReturn))) + positions.append(self.hash(self.untranslate(stringToReturn + '7'))) return positions @@ -273,7 +306,8 @@ def to_string(self, position: int, mode: StringMode) -> str: """ string_rep = self.translate(self.unhash(position)) waveString = string_rep[:self.row_length * self.num_rows] - boatString = string_rep[self.row_length * self.num_rows:] + boatString = string_rep[self.row_length * self.num_rows:self.row_length * self.num_rows + 4] + indicator = string_rep[self.row_length * self.num_rows + 4:] # pretend that the center of each tile is a wave or not if mode == StringMode.AUTOGUI: @@ -304,7 +338,7 @@ def to_string(self, position: int, mode: StringMode) -> str: for string in boat: stringBoat += string - return "1_" + stringWave + stringBoat + return "1_" + stringWave + stringBoat + indicator else: @@ -331,7 +365,7 @@ def to_string(self, position: int, mode: StringMode) -> str: i += 2 string_view = ''.join(string_view) - return string_view + return string_view + indicator def from_string(self, strposition: str) -> int: """ @@ -344,6 +378,7 @@ def from_string(self, strposition: str) -> int: rows = [strposition[i:i+self.row_length] for i in range(0, expected_length, self.row_length)] binary_str = "" + indicator = strposition[expected_length:] boat_positions = {} # color -> (row, col) of bottom cell for row_idx, row in enumerate(rows): @@ -368,7 +403,7 @@ def from_string(self, strposition: str) -> int: # Combine binary board + boat positions, then untranslate and hash full_string = binary_str + "".join(boat_pos_list) - return self.hash(self.untranslate(full_string)) + return self.hash(self.untranslate(full_string + indicator)) def move_to_string(self, move: int, mode: StringMode) -> str: @@ -376,17 +411,20 @@ def move_to_string(self, move: int, mode: StringMode) -> str: move_rep = self.translate(self.unhash(move)) move_rows = [move_rep[i:i+self.row_length] for i in range(0, self.row_length * self.num_rows, self.row_length)] - move_boat_str = move_rep[self.row_length * self.num_rows:] + move_boat_str = move_rep[self.row_length * self.num_rows:self.row_length * self.num_rows + 4] move_boats = [move_boat_str[i:i+2] for i in range(0, len(move_boat_str), 2)] + move_indicator = int(move_rep[self.row_length * self.num_rows + 4:]) curr_rows = self.board_rows curr_boats = self.boat_pos - # Check if any boat moved - for boat_i, (curr_bp, move_bp) in enumerate(zip(curr_boats, move_boats)): + # Check if any red boat moved + if move_indicator == 8 or move_indicator == 7: + curr_bp = curr_boats[0] + move_bp = move_boats[0] curr_row, curr_col = int(curr_bp[0]), int(curr_bp[1]) move_row, move_col = int(move_bp[0]), int(move_bp[1]) - color = self.colors[boat_i].lower() + color = self.colors[0].lower() if curr_col != move_col or curr_row != move_row: if curr_row != move_row: @@ -403,17 +441,40 @@ def move_to_string(self, move: int, mode: StringMode) -> str: return f"M_{start}_{end}_x" + #check if any blue boat moved + if move_indicator == 9 or move_indicator == 7: + curr_bp = curr_boats[1] + move_bp = move_boats[1] + curr_row, curr_col = int(curr_bp[0]), int(curr_bp[1]) + move_row, move_col = int(move_bp[0]), int(move_bp[1]) + color = self.colors[1].lower() + + if curr_col != move_col or curr_row != move_row: + if curr_row != move_row: + direction = "down" if move_row > curr_row else "up" + start = curr_row * self.row_length + curr_col + 35 + end = move_row * self.row_length + move_col + 35 + + return f"M_{start}_{end}_x" + + else: + direction = "left" if move_col < curr_col else "right" + start = curr_row * self.row_length + curr_col + 35 + end = move_row * self.row_length + move_col + 35 + + return f"M_{start}_{end}_x" + # Otherwise a wave row moved for row_i, (curr_row, move_row) in enumerate(zip(curr_rows, move_rows)): if curr_row != move_row: if move_row == curr_row[1:] + "0": # left arrow - start = row_i * self.row_length + 1 - end = row_i * self.row_length + start = move_indicator * self.row_length + 1 + end = move_indicator * self.row_length return f"M_{start}_{end}_x" else: # right arrow - start = row_i * self.row_length + 5 - end = row_i * self.row_length + 6 + start = move_indicator * self.row_length + 5 + end = move_indicator * self.row_length + 6 return f"M_{start}_{end}_x" return str(move) @@ -424,6 +485,7 @@ def move_to_string(self, move: int, mode: StringMode) -> str: move_rows = [move_rep[i:i+self.row_length] for i in range(0, self.row_length * self.num_rows, self.row_length)] move_boat_str = move_rep[self.row_length * self.num_rows:] move_boats = [move_boat_str[i:i+2] for i in range(0, len(move_boat_str), 2)] + move_indicator = int(move_rep[self.row_length * self.num_rows + 4:]) curr_rows = self.board_rows curr_boats = self.boat_pos @@ -442,14 +504,14 @@ def move_to_string(self, move: int, mode: StringMode) -> str: else: direction = "left" if move_col < curr_col else "right" return f"boat{color}-{direction}" - + # Otherwise a wave row moved for row_i, (curr_row, move_row) in enumerate(zip(curr_rows, move_rows)): if curr_row != move_row: if move_row == curr_row[1:] + "0": - return f"row{row_i}-left" + return f"row{move_indicator}-left" else: - return f"row{row_i}-right" + return f"row{move_indicator}-right" return str(move) # fallback @@ -463,47 +525,39 @@ def hash(self, strPos: str) -> int: # put string back together, then the final string convert into an integer via int(x, 3) wavePosString = strPos[:self.num_rows] - boatString = strPos[self.num_rows:] + boatString = strPos[self.num_rows:self.num_rows+ 4] + indicatorString = strPos[self.num_rows + 4:] boatTernaryString = "" + indicatorTernaryString = self.indicatorTernary(indicatorString) for i in range(0, len(boatString), 2): boat = boatString[i:i+2] boatTernaryString += self.boatTernary(boat) - result = wavePosString + boatTernaryString + result = wavePosString + boatTernaryString + indicatorTernaryString return int(result, 3) def unhash(self, intPos: int) -> str: boat_count = len(self.colors) - total_length = self.num_rows + 5 * boat_count # 5 shifts + 5 ternary digits per boat - # Ensure we have a plain Python int and that it fits in the expected - # number of ternary digits. If the value is out-of-range, raise a - # clear error instead of allowing `toTernaryString` to allocate an - # extremely large list and trigger MemoryError. - try: - int_val = int(intPos) - except Exception: - raise ValueError(f"unhash: position must be an integer-like value, got: {type(intPos)}") - - if int_val < 0: - raise ValueError(f"unhash: negative positions are invalid: {int_val}") - - max_value = 3 ** total_length - 1 - if int_val > max_value: - raise ValueError(f"unhash: position {int_val} exceeds max representable value {max_value} for total_length {total_length}") + total_length = self.num_rows + 5 * boat_count + 3 # 5 shifts + 5 ternary digits per boat + 3 for indicator + + int_val = int(intPos) strPos = self.toTernaryString(int_val).rjust(total_length, "0") wavePosString = strPos[:self.num_rows] - boatTernaryString = strPos[self.num_rows:] + boatTernaryString = strPos[self.num_rows:self.num_rows + 5 * boat_count] + indicatorTernaryString = strPos[self.num_rows + 5 * boat_count:] + boatString = "" + indicatorString = self.indicatorTernaryReverse(indicatorTernaryString) while boatTernaryString != "": boatString += self.boatTernaryReverse(boatTernaryString[:5]) boatTernaryString = boatTernaryString[5:] - return wavePosString + boatString + return wavePosString + boatString + indicatorString def boatTernary(self, boatString: str) -> str: # Convert 2-digit decimal boat position to fixed 5-digit ternary @@ -514,6 +568,16 @@ def boatTernary(self, boatString: str) -> str: def boatTernaryReverse(self, boatTernaryStr: str) -> str: # Convert 5-digit ternary back to 2-digit decimal string, zero-padded return str(int(boatTernaryStr, 3)).rjust(2, "0") + + def indicatorTernary(self, indicator): + # max is 1-9 integers used + print(indicator) + indicatorTern = self.toTernaryString(int(indicator)).rjust(3, "0") + return indicatorTern + + def indicatorTernaryReverse(self, boatTernaryStr: str) -> str: + # Convert 3-digit ternary back to 1-digit decimal string, zero-padded + return str(int(boatTernaryStr, 3)).rjust(1, "0") def toTernaryString(self, n): @@ -550,7 +614,8 @@ def untranslate(self, str): board_part = str[:(self.num_rows * self.row_length)] - boat_part = str[self.num_rows * self.row_length:] + boat_part = str[self.num_rows * self.row_length:self.num_rows * self.row_length + 4] + indicator = str[self.num_rows * self.row_length + 4:] shifts = "" for row_idx in range(self.num_rows): @@ -564,5 +629,5 @@ def untranslate(self, str): elif row == "00" + expected_row[:self.row_length - 2]: shifts += "2" - - return shifts + boat_part + print (shifts + boat_part + indicator) + return shifts + boat_part + indicator From 0ed0259cfa0e97f763f184250b636897df558720 Mon Sep 17 00:00:00 2001 From: Manlin Zhang Date: Mon, 1 Jun 2026 13:44:18 -0700 Subject: [PATCH 29/29] the string moves on the right side of GUI now work --- games/src/games/stormyseas.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/games/src/games/stormyseas.py b/games/src/games/stormyseas.py index f7ce4b0..a2531dc 100644 --- a/games/src/games/stormyseas.py +++ b/games/src/games/stormyseas.py @@ -491,22 +491,40 @@ def move_to_string(self, move: int, mode: StringMode) -> str: curr_boats = self.boat_pos # Check if any boat moved - for boat_i, (curr_bp, move_bp) in enumerate(zip(curr_boats, move_boats)): + + # red boat moved + if move_indicator == 8 or move_indicator == 7: + curr_bp = curr_boats[0] + move_bp = move_boats[0] curr_row, curr_col = int(curr_bp[0]), int(curr_bp[1]) move_row, move_col = int(move_bp[0]), int(move_bp[1]) - color = self.colors[boat_i].lower() if curr_col != move_col or curr_row != move_row: if curr_row != move_row: direction = "down" if move_row > curr_row else "up" - - return f"boat{color}-{direction}" + return f"boatr-{direction}" else: direction = "left" if move_col < curr_col else "right" - return f"boat{color}-{direction}" + return f"boatr-{direction}" + #blue boat moved + #check if any blue boat moved + + if move_indicator == 9 or move_indicator == 7: + curr_bp = curr_boats[1] + move_bp = move_boats[1] + curr_row, curr_col = int(curr_bp[0]), int(curr_bp[1]) + move_row, move_col = int(move_bp[0]), int(move_bp[1]) + if curr_col != move_col or curr_row != move_row: + if curr_row != move_row: + direction = "down" if move_row > curr_row else "up" + return f"boatb-{direction}" + else: + direction = "left" if move_col < curr_col else "right" + return f"boatb-{direction}" + # Otherwise a wave row moved - for row_i, (curr_row, move_row) in enumerate(zip(curr_rows, move_rows)): + for row_i, (curr_row, move_row) in enumerate(zip(curr_rows, move_rows)): if curr_row != move_row: if move_row == curr_row[1:] + "0": return f"row{move_indicator}-left"