Refactor (#8)

Co-authored-by: Donovan <donovan.a.kelly@pm.me>
Reviewed-on: https://git.infra.nkode.tech/dkelly/evilnkode/pulls/8
This commit is contained in:
dkelly
2025-09-09 18:22:22 +00:00
parent 6c07d62cbe
commit e24fe3b512
121 changed files with 1259 additions and 8398 deletions

0
src/keypad/__init__.py Normal file
View File

123
src/keypad/keypad.py Normal file
View File

@@ -0,0 +1,123 @@
from dataclasses import dataclass
import numpy as np
from src.keypad.tower_shuffle import TowerShuffle
from abc import ABC, abstractmethod
from typing import Self
class BaseKeypad(ABC):
keypad: np.ndarray
k: int # number of keys
p: int # properties per key
@classmethod
def _build_keypad(cls, k: int, p: int) -> np.ndarray:
rng = np.random.default_rng()
total = k * p
array = np.arange(total)
keypad = array.reshape(k, p)
set_view = keypad.T.copy()
for set_idx in set_view:
rng.shuffle(set_idx)
return set_view.T
@classmethod
@abstractmethod
def new_keypad(cls, k: int, p: int) -> Self:
raise NotImplementedError
def key_entry(self, target_passcode: list[int]) -> list[int]:
vals = np.array(target_passcode)
if np.any((vals < 0) | (vals >= self.k * self.p)):
raise ValueError("One or more values are out of the valid range.")
flat = self.keypad.flatten()
inv_index = np.empty(self.k * self.p, dtype=int)
for i, v in enumerate(flat):
inv_index[v] = i // self.p
return inv_index[vals].tolist()
@abstractmethod
def shuffle(self):
pass
def keypad_mat(self) -> list[list[int]]:
return [el.tolist() for el in self.keypad]
@dataclass
class SlidingTowerShuffleKeypad(BaseKeypad):
keypad: np.ndarray
k: int # number of keys
p: int # properties per key
tower_shuffle: TowerShuffle
@classmethod
def new_keypad(cls, k: int, p: int) -> Self:
kp = cls._build_keypad(k, p)
return cls(keypad=kp, k=k, p=p, tower_shuffle=TowerShuffle.new(p))
def shuffle(self):
selected_positions = self.tower_shuffle.left_tower.tolist()
shift = np.random.randint(1, self.k) # random int in [1, k-1]
new_key_idxs = np.roll(np.arange(self.k), shift)
shuffled_sets = self.keypad.copy()
shuffled_sets[:, selected_positions] = shuffled_sets[new_key_idxs, :][:, selected_positions]
self.keypad = shuffled_sets
self.tower_shuffle.shuffle()
@dataclass
class RandomShuffleKeypad(BaseKeypad):
keypad: np.ndarray
k: int # number of keys
p: int # properties per key
@classmethod
def new_keypad(cls, k: int, p: int) -> Self:
kp = cls._build_keypad(k, p)
return cls(keypad=kp, k=k, p=p)
def shuffle(self):
shuffled_matrix = np.array([np.random.permutation(row) for row in self.keypad.T])
self.keypad = shuffled_matrix.T
@dataclass
class RandomSplitShuffleKeypad(BaseKeypad):
keypad: np.ndarray
k: int # number of keys
p: int # properties per key
@classmethod
def new_keypad(cls, k: int, p: int) -> Self:
kp = cls._build_keypad(k, p)
return cls(keypad=kp, k=k, p=p)
def shuffle(self):
column_permutation = np.random.permutation(self.p)
column_subset = column_permutation[:self.p // 2]
new_key_idxs = np.random.permutation(self.k)
shuffled_sets = self.keypad.copy()
shuffled_sets[:, column_subset] = shuffled_sets[new_key_idxs, :][:, column_subset]
self.keypad = shuffled_sets
@dataclass
class SlidingSplitShuffleKeypad(BaseKeypad):
keypad: np.ndarray
k: int # number of keys
p: int # properties per key
@classmethod
def new_keypad(cls, k: int, p: int) -> Self:
kp = cls._build_keypad(k, p)
return cls(keypad=kp, k=k, p=p)
def shuffle(self):
selected_positions = np.random.permutation(self.p)
column_subset = selected_positions[:self.p // 2]
shift = np.random.randint(1, self.k)
new_key_idxs = np.roll(np.arange(self.k), shift)
shuffled_sets = self.keypad.copy()
shuffled_sets[:, column_subset] = shuffled_sets[new_key_idxs, :][:, column_subset]
self.keypad = shuffled_sets

View File

@@ -0,0 +1,80 @@
from dataclasses import dataclass
import numpy as np
@dataclass
class Tower:
floors: list[np.ndarray]
def split_tower(self) -> tuple[np.ndarray, np.ndarray]:
discard = np.array([], dtype=int)
keep = np.array([], dtype=int)
balance = self.balance()
for idx, floor in enumerate(self.floors):
div = len(floor)//2 + balance[idx]
floor_shuffle = np.random.permutation(len(floor))
keep = np.concatenate((keep, floor[floor_shuffle[:div]]))
discard = np.concatenate((discard, floor[floor_shuffle[div:]]))
diff = len(discard) - len(keep)
assert 0 <= diff <= 1
return keep, discard
def balance(self) -> list[int]:
odd_floors = np.array([idx for idx, el in enumerate(self.floors) if len(el) & 1])
balance = np.zeros(len(self.floors), dtype=int)
if len(odd_floors) == 0:
return balance.tolist()
shuffle = np.random.permutation(len(odd_floors))[:len(odd_floors) // 2]
odd_floors = odd_floors[shuffle]
balance[odd_floors] = 1
return balance.tolist()
def update_tower(self, keep: np.ndarray, other_discard: np.ndarray):
new_floors = []
for floor in self.floors:
new_floor = np.intersect1d(floor, keep)
if len(new_floor):
new_floors.append(new_floor)
self.floors = new_floors
self.floors.insert(0, other_discard)
def __str__(self):
str_val = ""
floor_numb = [i for i in reversed(range(len(self.floors)))]
for idx, val in enumerate(reversed(self.floors)):
str_val += f"Floor {floor_numb[idx]}: {val.tolist()}\n"
return str_val
def tolist(self) -> list[int]:
tower = []
for floor in self.floors:
tower.extend(floor.tolist())
return tower
@dataclass
class TowerShuffle:
total_positions: int
left_tower: Tower
right_tower: Tower
@classmethod
def new(cls, total_pos:int):
assert total_pos >= 3
rand_pos = np.random.permutation(total_pos)
return TowerShuffle(
total_positions=total_pos,
left_tower=Tower(floors=[rand_pos[:total_pos//2]]),
right_tower=Tower(floors=[rand_pos[total_pos//2:]]),
)
def shuffle(self):
left_keep, left_discard = self.left_tower.split_tower()
right_keep, right_discard = self.right_tower.split_tower()
self.left_tower.update_tower(left_keep, right_discard)
self.right_tower.update_tower(right_keep, left_discard)
def __str__(self):
return f"""Left Tower:
{self.left_tower}
Right Tower:
{self.right_tower}"""