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:
0
src/keypad/__init__.py
Normal file
0
src/keypad/__init__.py
Normal file
123
src/keypad/keypad.py
Normal file
123
src/keypad/keypad.py
Normal 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
|
||||
80
src/keypad/tower_shuffle.py
Normal file
80
src/keypad/tower_shuffle.py
Normal 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}"""
|
||||
Reference in New Issue
Block a user