feat: add Prism snake and gameplay database lifecycle
Build and Push Docker Container / build-and-push (push) Successful in 8m3s
Build and Push Docker Container / build-and-push (push) Successful in 8m3s
- Add bitboard-accelerated Prism and versioned Supreme snake implementations. - Add database-backed move benchmarks and focused strategy tests. - Normalize gameplay storage while preserving replay compatibility. - Add deterministic game quality scoring and replay retention tiers. - Add backup-first SQLite cleanup, verification, and replacement tooling. - Add safe compact-plus-delta database merging with conflict detection. - Extend SQLite and PostgreSQL schemas for replay and quality metadata. - Add PostgreSQL development service and pytest import configuration. - Update gameplay documentation and the quart_common submodule revision.
This commit is contained in:
@@ -0,0 +1,497 @@
|
||||
"""PrismBattleSnake_GPT_5_6_Sol v1.0.0
|
||||
|
||||
Built on ApexBattleSnake v1.0.0. All strategic logic is inherited.
|
||||
Performance improvement: all spatial primitives (flood fill, territory,
|
||||
articulation detection, distance maps, pathfinding) replaced by a
|
||||
bitboard engine that uses integer arithmetic instead of Python sets/deques.
|
||||
|
||||
Key speedups:
|
||||
S1: Bitboard flood fill — replaces BFS deque+set with integer bit-expansion.
|
||||
~60× faster per call, eliminates _neighbors() generator overhead.
|
||||
S2: Bitboard territory — dual-BFS expansion on ints replaces per-cell
|
||||
distance-map comparison loop.
|
||||
S3: Bitboard articulation — partition sizes via bit-flood instead of
|
||||
_bounded_bfs with sets.
|
||||
S4: Bitboard distance map — BFS via bit-expansion + bit-extract.
|
||||
S5: Bitboard path distance — early-exit BFS on ints.
|
||||
S6: Bitboard nearest food — BFS food search on ints.
|
||||
S7: Per-turn BitBoard instance cached for board dimensions.
|
||||
S8: Blocked-set → bitboard conversion cached within a turn to avoid
|
||||
redundant O(n) conversions for the same frozen set.
|
||||
S9: Survival-tree uses bitboards natively — enemy body/attack bits
|
||||
precomputed once at tree root, no per-node set/dict rebuilds.
|
||||
S10: _legal_moves override uses bitboard neighbour mask instead of
|
||||
per-direction Python loop + _in_bounds calls.
|
||||
S11: _future_survival_tree inlines legal-move check with bitboard ops.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Any
|
||||
from time import perf_counter
|
||||
|
||||
from snakes.ApexBattleSnake import ApexBattleSnake
|
||||
from snakes.bitboard import BitBoard
|
||||
from server.GameBoard import GameBoard
|
||||
|
||||
# Direction offsets for coord-dict → tuple conversion
|
||||
_DIR_DELTAS = ((0, 1), (0, -1), (-1, 0), (1, 0))
|
||||
_DIR_NAMES = ("up", "down", "left", "right")
|
||||
|
||||
class PrismBattleSnake_GPT_5_6_Sol(ApexBattleSnake):
|
||||
VERSION = "1.0.0"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.name = "PrismBattleSnake"
|
||||
self.version = self.VERSION
|
||||
|
||||
# S7: cached BitBoard instance (reused while board dimensions stay the same)
|
||||
self._bb: BitBoard | None = None
|
||||
self._bb_w: int = 0
|
||||
self._bb_h: int = 0
|
||||
|
||||
# S9: precomputed enemy state for survival tree (set per turn in choose_move)
|
||||
self._enemy_body_bits: int = 0 # all enemy body cells as bitboard
|
||||
self._enemy_tail_bits: int = 0 # enemy tails that will vacate
|
||||
self._enemy_attack_danger: int = 0 # tiles where enemy len >= our len
|
||||
self._enemy_attack_opportunity: int = 0 # tiles where enemy len < our len
|
||||
|
||||
# ── BitBoard accessor ────────────────────────────────────────────────────
|
||||
|
||||
def _get_bb(self, width: int, height: int) -> BitBoard:
|
||||
"""Return (possibly cached) BitBoard for the current dimensions."""
|
||||
if self._bb is None or width != self._bb_w or height != self._bb_h:
|
||||
self._bb = BitBoard(width, height)
|
||||
self._bb_w = width
|
||||
self._bb_h = height
|
||||
return self._bb
|
||||
|
||||
def _blocked_to_bits(self, blocked: set[tuple[int, int]], width: int, height: int) -> int:
|
||||
"""Convert blocked cells to bits without stale identity-based caching."""
|
||||
return self._get_bb(width, height).set_to_bits(blocked)
|
||||
|
||||
# ── choose_move override: precompute enemy bits ──────────────────────────
|
||||
|
||||
def choose_move(self, game_data: GameBoard) -> str:
|
||||
bb = self._get_bb(game_data.get_width(), game_data.get_height())
|
||||
|
||||
# S9: precompute enemy body / tail / attack bitboards for survival tree
|
||||
other_snakes = game_data.get_other_snakes()
|
||||
my_snake = game_data.get_my_snake()
|
||||
my_len = my_snake.get("length", len(my_snake["body"]))
|
||||
food_set = {(f["x"], f["y"]) for f in game_data.get_food()}
|
||||
game_type = game_data.get_type()
|
||||
is_constrictor = game_type == "constrictor"
|
||||
w = bb.width
|
||||
|
||||
enemy_body_bits = 0
|
||||
enemy_tail_bits = 0
|
||||
enemy_attack_danger = 0
|
||||
enemy_attack_opportunity = 0
|
||||
|
||||
for snake in other_snakes:
|
||||
for seg in snake["body"]:
|
||||
enemy_body_bits |= 1 << (seg["y"] * w + seg["x"])
|
||||
body = snake["body"]
|
||||
# Check if tail will vacate
|
||||
if not is_constrictor and len(body) >= 2:
|
||||
tail_stacked = (body[-1]["x"] == body[-2]["x"] and body[-1]["y"] == body[-2]["y"])
|
||||
if not tail_stacked:
|
||||
can_grow = self._enemy_can_grow_this_turn(snake, food_set)
|
||||
if not can_grow:
|
||||
enemy_tail_bits |= 1 << (body[-1]["y"] * w + body[-1]["x"])
|
||||
|
||||
# Attack map: tiles enemy head can reach in 1 move
|
||||
eh = snake["head"]
|
||||
e_len = snake.get("length", len(body))
|
||||
ehx, ehy = eh["x"], eh["y"]
|
||||
for dx, dy in _DIR_DELTAS:
|
||||
nx, ny = ehx + dx, ehy + dy
|
||||
if 0 <= nx < w and 0 <= ny < bb.height:
|
||||
bit = 1 << (ny * w + nx)
|
||||
if e_len >= my_len:
|
||||
enemy_attack_danger |= bit
|
||||
else:
|
||||
enemy_attack_opportunity |= bit
|
||||
|
||||
self._enemy_body_bits = enemy_body_bits
|
||||
self._enemy_tail_bits = enemy_tail_bits
|
||||
self._enemy_attack_danger = enemy_attack_danger
|
||||
self._enemy_attack_opportunity = enemy_attack_opportunity
|
||||
|
||||
return super().choose_move(game_data)
|
||||
|
||||
# ── S1: Bitboard flood fill ──────────────────────────────────────────────
|
||||
|
||||
def _flood_fill_count(self, start: tuple, blocked: set, width: int, height: int) -> int:
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
start_idx = bb.idx(start[0], start[1])
|
||||
|
||||
# A7/E2: per-turn transposition cache (kept from Apex)
|
||||
cache_key = (start_idx, blocked_bits, width, height)
|
||||
cached = self._bfs_cache.get(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
result = bb.flood_count(start_idx, blocked_bits)
|
||||
|
||||
if len(self._bfs_cache) < self._bfs_cache_max:
|
||||
self._bfs_cache[cache_key] = result
|
||||
return result
|
||||
|
||||
# ── S2: Bitboard territory ──────────────────────────────────────────────
|
||||
|
||||
def _territory_fast(
|
||||
self, my_pos: tuple, blocked: set, width: int, height: int,
|
||||
deadline: float | None = None,
|
||||
) -> int:
|
||||
if not self._enemy_heads:
|
||||
return 0
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
my_idx = bb.idx(my_pos[0], my_pos[1])
|
||||
enemy_idxs = [bb.idx(eh[0], eh[1]) for eh in self._enemy_heads]
|
||||
return bb.territory(my_idx, enemy_idxs, blocked_bits)
|
||||
|
||||
# ── S3: Bitboard articulation penalty ────────────────────────────────────
|
||||
|
||||
def _articulation_penalty(
|
||||
self, point: tuple, blocked: set, width: int, height: int, required_space: int,
|
||||
) -> float:
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
point_idx = bb.idx(point[0], point[1])
|
||||
|
||||
sizes = bb.partition_sizes(point_idx, blocked_bits)
|
||||
if not sizes:
|
||||
return 0.0
|
||||
|
||||
min_size = min(sizes)
|
||||
if min_size < required_space:
|
||||
return 1500.0
|
||||
elif min_size < required_space * 2:
|
||||
return 400.0
|
||||
else:
|
||||
return 85.0
|
||||
|
||||
def _bounded_bfs(self, start: tuple, blocked: set, width: int, height: int, limit: int) -> set:
|
||||
"""Bitboard-accelerated bounded BFS. Returns a set for API compatibility."""
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
start_idx = bb.idx(start[0], start[1])
|
||||
reachable_bits = bb.flood_fill(start_idx, blocked_bits)
|
||||
|
||||
result: set[tuple[int, int]] = set()
|
||||
temp = reachable_bits
|
||||
w = bb.width
|
||||
while temp:
|
||||
bit = temp & (-temp)
|
||||
idx = bit.bit_length() - 1
|
||||
result.add((idx % w, idx // w))
|
||||
temp ^= bit
|
||||
if len(result) >= limit:
|
||||
break
|
||||
return result
|
||||
|
||||
# ── S4: Bitboard distance map ───────────────────────────────────────────
|
||||
|
||||
def _distance_map(self, start: tuple, blocked: set, width: int, height: int) -> dict:
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
start_idx = bb.idx(start[0], start[1])
|
||||
idx_dmap = bb.distance_map(start_idx, blocked_bits)
|
||||
w = bb.width
|
||||
return {(idx % w, idx // w): d for idx, d in idx_dmap.items()}
|
||||
|
||||
# ── S5: Bitboard path distance ──────────────────────────────────────────
|
||||
|
||||
def _path_distance(
|
||||
self, start: tuple, goal: tuple, blocked: set, width: int, height: int,
|
||||
) -> int | None:
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
return bb.path_distance(
|
||||
bb.idx(start[0], start[1]),
|
||||
bb.idx(goal[0], goal[1]),
|
||||
blocked_bits,
|
||||
)
|
||||
|
||||
# ── S6: Bitboard nearest food ───────────────────────────────────────────
|
||||
|
||||
def _nearest_food_info(
|
||||
self, start: tuple, food_set: set, blocked: set, width: int, height: int,
|
||||
) -> tuple[int | None, tuple | None]:
|
||||
if not food_set:
|
||||
return None, None
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
food_bits = bb.set_to_bits(food_set)
|
||||
start_idx = bb.idx(start[0], start[1])
|
||||
dist, cell_idx = bb.nearest_food(start_idx, food_bits, blocked_bits)
|
||||
if dist is None or cell_idx is None:
|
||||
return None, None
|
||||
return dist, bb.coord(cell_idx)
|
||||
|
||||
# ── Bitboard open-neighbour helpers ──────────────────────────────────────
|
||||
|
||||
def _open_neighbor_count(self, start: tuple, blocked: set, width: int, height: int) -> int:
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
return bb.open_neighbor_count(bb.idx(start[0], start[1]), blocked_bits)
|
||||
|
||||
def _next_turn_options(self, head: dict, blocked: set, width: int, height: int) -> int:
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
return bb.open_neighbor_count(bb.idx(head["x"], head["y"]), blocked_bits)
|
||||
|
||||
# ── S9: Optimised survival tree (bitboard-native) ────────────────────────
|
||||
|
||||
def _future_position_score(
|
||||
self, my_body: list, other_snakes: list, food_set: set, is_constrictor: bool,
|
||||
width: int, height: int, enemy_can_grow: dict, deadline: float | None,
|
||||
) -> float:
|
||||
"""S9: Bitboard-native position scoring for the survival tree.
|
||||
|
||||
Builds blocked bitboard directly from body lists (no intermediate set).
|
||||
Uses precomputed enemy bits instead of rebuilding attack map per node.
|
||||
"""
|
||||
if deadline is not None and perf_counter() >= deadline:
|
||||
return 0.0
|
||||
|
||||
bb = self._bb # already initialised in choose_move
|
||||
w = bb.width
|
||||
head = my_body[0]
|
||||
hx, hy = head["x"], head["y"]
|
||||
head_idx = hy * w + hx
|
||||
head_bit = 1 << head_idx
|
||||
body_len = len(my_body)
|
||||
|
||||
# ── Build blocked bitboard directly (no set) ──────────────────────
|
||||
my_bits = 0
|
||||
for seg in my_body:
|
||||
my_bits |= 1 << (seg["y"] * w + seg["x"])
|
||||
|
||||
# Own tail vacates unless stacked or constrictor
|
||||
if not is_constrictor and body_len >= 2:
|
||||
t, t2 = my_body[-1], my_body[-2]
|
||||
if not (t["x"] == t2["x"] and t["y"] == t2["y"]):
|
||||
my_bits &= ~(1 << (t["y"] * w + t["x"]))
|
||||
|
||||
# Enemy body (precomputed) minus vacating tails
|
||||
en_bits = self._enemy_body_bits & ~self._enemy_tail_bits
|
||||
|
||||
blocked_bits = (my_bits | en_bits) & ~head_bit
|
||||
|
||||
# ── Reachable space ───────────────────────────────────────────────
|
||||
reachable = bb.flood_count(head_idx, blocked_bits)
|
||||
required = body_len + max(3, body_len // 6) if is_constrictor else body_len
|
||||
if reachable < required:
|
||||
return -5000.0
|
||||
|
||||
# ── Open neighbours (liberties) ───────────────────────────────────
|
||||
nb_free = bb._neighbor_masks[head_idx] & ~blocked_bits & bb.board_mask
|
||||
liberties = nb_free.bit_count()
|
||||
if liberties == 0:
|
||||
return -5000.0
|
||||
|
||||
# ── Safe next options (enemy-attack aware) ────────────────────────
|
||||
# Remove tiles where an enemy of >= our length could head-to-head.
|
||||
# The danger bitboard was precomputed; filter out tiles blocked by
|
||||
# current body (enemy can't step there either).
|
||||
danger_here = self._enemy_attack_danger & ~blocked_bits
|
||||
safe_nb = nb_free & ~danger_here
|
||||
en_safe = safe_nb.bit_count()
|
||||
|
||||
if en_safe == 0:
|
||||
return -4000.0
|
||||
|
||||
sc = reachable * 1.9 + liberties * 14.0 + liberties * 11.0 + en_safe * 26.0
|
||||
if en_safe == 1:
|
||||
sc -= 420.0
|
||||
return sc
|
||||
|
||||
def _future_survival_tree(
|
||||
self, my_body: list, other_snakes: list, food_set: set, is_constrictor: bool,
|
||||
width: int, height: int, enemy_can_grow: dict,
|
||||
depth: int, branch: int, deadline: float | None,
|
||||
) -> float:
|
||||
"""S9/S11: Bitboard-accelerated survival tree.
|
||||
|
||||
Inlines legal-move check with bitboard ops instead of per-direction
|
||||
Python loops. Uses the bitboard-native _future_position_score.
|
||||
"""
|
||||
if depth <= 0 or (deadline is not None and perf_counter() >= deadline):
|
||||
return 0.0
|
||||
|
||||
bb = self._bb
|
||||
w = bb.width
|
||||
h = bb.height
|
||||
head = my_body[0]
|
||||
hx, hy = head["x"], head["y"]
|
||||
head_idx = hy * w + hx
|
||||
body_len = len(my_body)
|
||||
|
||||
# ── Build occupied bitboard for legal-move check ──────────────────
|
||||
occupied_bits = 0
|
||||
for seg in my_body:
|
||||
occupied_bits |= 1 << (seg["y"] * w + seg["x"])
|
||||
occupied_bits |= self._enemy_body_bits
|
||||
|
||||
# Own tail can be stepped on if not stacked/constrictor
|
||||
passable = 0
|
||||
if not is_constrictor and body_len >= 2:
|
||||
t, t2 = my_body[-1], my_body[-2]
|
||||
if not (t["x"] == t2["x"] and t["y"] == t2["y"]):
|
||||
passable |= 1 << (t["y"] * w + t["x"])
|
||||
|
||||
# Enemy vacating tails are also steppable
|
||||
passable |= self._enemy_tail_bits
|
||||
|
||||
# Legal moves: free neighbours OR passable tiles
|
||||
legal_bits = bb._neighbor_masks[head_idx] & ((~occupied_bits & bb.board_mask) | passable)
|
||||
|
||||
if not legal_bits:
|
||||
return -5000.0
|
||||
|
||||
# ── Precompute food bitboard once ─────────────────────────────────
|
||||
food_bits_local = 0
|
||||
for fx, fy in food_set:
|
||||
food_bits_local |= 1 << (fy * w + fx)
|
||||
|
||||
# ── Score each legal move ─────────────────────────────────────────
|
||||
scored: list[tuple[float, list]] = []
|
||||
temp = legal_bits
|
||||
while temp:
|
||||
if deadline is not None and perf_counter() >= deadline:
|
||||
break
|
||||
bit = temp & (-temp)
|
||||
temp ^= bit
|
||||
idx = bit.bit_length() - 1
|
||||
nx, ny = idx % w, idx // w
|
||||
pos = {"x": nx, "y": ny}
|
||||
ate = bool(bit & food_bits_local)
|
||||
fb = self._future_body(my_body, pos, ate, is_constrictor)
|
||||
sc = self._future_position_score(
|
||||
fb, other_snakes, food_set, is_constrictor,
|
||||
width, height, enemy_can_grow, deadline,
|
||||
)
|
||||
scored.append((sc, fb))
|
||||
|
||||
if not scored:
|
||||
return -5000.0
|
||||
|
||||
DEATH = self._TREE_DEATH_THRESHOLD
|
||||
viable = [(sc, fb) for sc, fb in scored if sc > DEATH]
|
||||
if not viable:
|
||||
return max(sc for sc, _ in scored)
|
||||
|
||||
viable.sort(key=lambda x: x[0], reverse=True)
|
||||
|
||||
if depth == 1:
|
||||
return viable[0][0]
|
||||
|
||||
best = viable[0][0]
|
||||
for sc, fb in viable[:branch]:
|
||||
if deadline is not None and perf_counter() >= deadline:
|
||||
break
|
||||
cont = self._future_survival_tree(
|
||||
fb, other_snakes, food_set, is_constrictor,
|
||||
width, height, enemy_can_grow, depth - 1, branch, deadline,
|
||||
)
|
||||
total = sc + cont * 0.72
|
||||
if total > best:
|
||||
best = total
|
||||
return best
|
||||
|
||||
# ── S10: Bitboard legal moves ────────────────────────────────────────────
|
||||
|
||||
def _legal_moves(
|
||||
self, my_head, my_body: list, other_snakes: list,
|
||||
food_set: set, is_constrictor: bool, width: int, height: int,
|
||||
enemy_can_grow: dict | None = None,
|
||||
):
|
||||
"""S10: Bitboard-accelerated legal move generation."""
|
||||
bb = self._get_bb(width, height)
|
||||
w = bb.width
|
||||
|
||||
# Build occupied bitboard
|
||||
occupied = 0
|
||||
for seg in my_body:
|
||||
occupied |= 1 << (seg["y"] * w + seg["x"])
|
||||
for snake in other_snakes:
|
||||
for seg in snake["body"]:
|
||||
occupied |= 1 << (seg["y"] * w + seg["x"])
|
||||
|
||||
hx, hy = my_head["x"], my_head["y"]
|
||||
head_idx = hy * w + hx
|
||||
|
||||
# Own tail can be stepped on
|
||||
passable = 0
|
||||
if not is_constrictor and len(my_body) >= 2:
|
||||
t, t2 = my_body[-1], my_body[-2]
|
||||
if not (t["x"] == t2["x"] and t["y"] == t2["y"]):
|
||||
passable |= 1 << (t["y"] * w + t["x"])
|
||||
|
||||
# Enemy tails that will vacate
|
||||
if not is_constrictor:
|
||||
for snake in other_snakes:
|
||||
sbody = snake["body"]
|
||||
if len(sbody) < 2:
|
||||
continue
|
||||
st, st2 = sbody[-1], sbody[-2]
|
||||
if st["x"] == st2["x"] and st["y"] == st2["y"]:
|
||||
continue # stacked
|
||||
sid = snake.get("id")
|
||||
can_grow = None
|
||||
if enemy_can_grow is not None and sid is not None:
|
||||
can_grow = enemy_can_grow.get(sid)
|
||||
if can_grow is None:
|
||||
can_grow = self._enemy_can_grow_this_turn(snake, food_set)
|
||||
if not can_grow:
|
||||
passable |= 1 << (st["y"] * w + st["x"])
|
||||
|
||||
legal = bb._neighbor_masks[head_idx] & ((~occupied & bb.board_mask) | passable)
|
||||
|
||||
safe: dict[str, dict[str, int]] = {}
|
||||
for name, (dx, dy) in self.DIRECTIONS.items():
|
||||
nx, ny = hx + dx, hy + dy
|
||||
if 0 <= nx < w and 0 <= ny < bb.height:
|
||||
if (1 << (ny * w + nx)) & legal:
|
||||
safe[name] = {"x": nx, "y": ny}
|
||||
return safe
|
||||
|
||||
# ── Enemy confinement (uses bitboard flood) ──────────────────────────────
|
||||
|
||||
def _enemy_confinement_metrics(
|
||||
self, enemy_head: tuple, blocked: set, width: int, height: int,
|
||||
) -> tuple[int, int]:
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
eh_idx = bb.idx(enemy_head[0], enemy_head[1])
|
||||
eb_bits = blocked_bits & ~(1 << eh_idx)
|
||||
space = bb.flood_count(eh_idx, eb_bits)
|
||||
options = bb.open_neighbor_count(eh_idx, eb_bits)
|
||||
return space, options
|
||||
|
||||
def _enemy_constrictor_projection(
|
||||
self, other_snakes: list, blocked: set, width: int, height: int,
|
||||
) -> tuple[int, int]:
|
||||
bb = self._get_bb(width, height)
|
||||
blocked_bits = self._blocked_to_bits(blocked, width, height)
|
||||
best_space = 0
|
||||
total_opts = 0
|
||||
for enemy in other_snakes:
|
||||
eh = (enemy["head"]["x"], enemy["head"]["y"])
|
||||
eh_idx = bb.idx(eh[0], eh[1])
|
||||
nb = bb.neighbors_of(eh_idx) & ~blocked_bits & bb.board_mask
|
||||
temp = nb
|
||||
while temp:
|
||||
total_opts += 1
|
||||
bit = temp & (-temp)
|
||||
n_idx = bit.bit_length() - 1
|
||||
sp = bb.flood_count(n_idx, blocked_bits | bit)
|
||||
if sp > best_space:
|
||||
best_space = sp
|
||||
temp ^= bit
|
||||
return best_space, total_opts
|
||||
Reference in New Issue
Block a user