glitchlings 0.10.2__cp312-cp312-macosx_11_0_universal2.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of glitchlings might be problematic. Click here for more details.
- glitchlings/__init__.py +99 -0
- glitchlings/__main__.py +8 -0
- glitchlings/_zoo_rust/__init__.py +12 -0
- glitchlings/_zoo_rust.cpython-312-darwin.so +0 -0
- glitchlings/assets/__init__.py +180 -0
- glitchlings/assets/apostrofae_pairs.json +32 -0
- glitchlings/assets/ekkokin_homophones.json +2014 -0
- glitchlings/assets/hokey_assets.json +193 -0
- glitchlings/assets/lexemes/academic.json +1049 -0
- glitchlings/assets/lexemes/colors.json +1333 -0
- glitchlings/assets/lexemes/corporate.json +716 -0
- glitchlings/assets/lexemes/cyberpunk.json +22 -0
- glitchlings/assets/lexemes/lovecraftian.json +23 -0
- glitchlings/assets/lexemes/synonyms.json +3354 -0
- glitchlings/assets/mim1c_homoglyphs.json.gz.b64 +1064 -0
- glitchlings/assets/ocr_confusions.tsv +30 -0
- glitchlings/assets/pipeline_assets.json +29 -0
- glitchlings/attack/__init__.py +147 -0
- glitchlings/attack/analysis.py +1321 -0
- glitchlings/attack/core.py +493 -0
- glitchlings/attack/core_execution.py +367 -0
- glitchlings/attack/core_planning.py +612 -0
- glitchlings/attack/encode.py +114 -0
- glitchlings/attack/metrics.py +218 -0
- glitchlings/attack/metrics_dispatch.py +70 -0
- glitchlings/attack/tokenization.py +227 -0
- glitchlings/auggie.py +284 -0
- glitchlings/compat/__init__.py +9 -0
- glitchlings/compat/loaders.py +355 -0
- glitchlings/compat/types.py +41 -0
- glitchlings/conf/__init__.py +41 -0
- glitchlings/conf/loaders.py +331 -0
- glitchlings/conf/schema.py +156 -0
- glitchlings/conf/types.py +72 -0
- glitchlings/config.toml +2 -0
- glitchlings/constants.py +59 -0
- glitchlings/dev/__init__.py +3 -0
- glitchlings/dev/docs.py +45 -0
- glitchlings/dlc/__init__.py +19 -0
- glitchlings/dlc/_shared.py +296 -0
- glitchlings/dlc/gutenberg.py +400 -0
- glitchlings/dlc/huggingface.py +68 -0
- glitchlings/dlc/prime.py +215 -0
- glitchlings/dlc/pytorch.py +98 -0
- glitchlings/dlc/pytorch_lightning.py +173 -0
- glitchlings/internal/__init__.py +16 -0
- glitchlings/internal/rust.py +159 -0
- glitchlings/internal/rust_ffi.py +490 -0
- glitchlings/main.py +426 -0
- glitchlings/protocols.py +91 -0
- glitchlings/runtime_config.py +24 -0
- glitchlings/util/__init__.py +27 -0
- glitchlings/util/adapters.py +65 -0
- glitchlings/util/keyboards.py +356 -0
- glitchlings/util/transcripts.py +108 -0
- glitchlings/zoo/__init__.py +161 -0
- glitchlings/zoo/assets/__init__.py +29 -0
- glitchlings/zoo/core.py +678 -0
- glitchlings/zoo/core_execution.py +154 -0
- glitchlings/zoo/core_planning.py +451 -0
- glitchlings/zoo/corrupt_dispatch.py +295 -0
- glitchlings/zoo/hokey.py +139 -0
- glitchlings/zoo/jargoyle.py +243 -0
- glitchlings/zoo/mim1c.py +148 -0
- glitchlings/zoo/pedant/__init__.py +109 -0
- glitchlings/zoo/pedant/core.py +105 -0
- glitchlings/zoo/pedant/forms.py +74 -0
- glitchlings/zoo/pedant/stones.py +74 -0
- glitchlings/zoo/redactyl.py +97 -0
- glitchlings/zoo/rng.py +259 -0
- glitchlings/zoo/rushmore.py +416 -0
- glitchlings/zoo/scannequin.py +66 -0
- glitchlings/zoo/transforms.py +346 -0
- glitchlings/zoo/typogre.py +128 -0
- glitchlings/zoo/validation.py +477 -0
- glitchlings/zoo/wherewolf.py +120 -0
- glitchlings/zoo/zeedub.py +93 -0
- glitchlings-0.10.2.dist-info/METADATA +337 -0
- glitchlings-0.10.2.dist-info/RECORD +83 -0
- glitchlings-0.10.2.dist-info/WHEEL +5 -0
- glitchlings-0.10.2.dist-info/entry_points.txt +3 -0
- glitchlings-0.10.2.dist-info/licenses/LICENSE +201 -0
- glitchlings-0.10.2.dist-info/top_level.txt +1 -0
glitchlings/zoo/mim1c.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Rust-backed Mim1c glitchling that swaps characters for homoglyphs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
from collections.abc import Collection, Iterable
|
|
7
|
+
from typing import Any, Literal, cast
|
|
8
|
+
|
|
9
|
+
from glitchlings.constants import DEFAULT_MIM1C_RATE, MIM1C_DEFAULT_CLASSES
|
|
10
|
+
from glitchlings.internal.rust_ffi import mim1c_rust, resolve_seed
|
|
11
|
+
|
|
12
|
+
from .core import AttackOrder, AttackWave, Glitchling, PipelineOperationPayload
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _normalise_classes(
|
|
16
|
+
value: object,
|
|
17
|
+
) -> tuple[str, ...] | Literal["all"] | None:
|
|
18
|
+
if value is None:
|
|
19
|
+
return None
|
|
20
|
+
if isinstance(value, str):
|
|
21
|
+
if value.lower() == "all":
|
|
22
|
+
return "all"
|
|
23
|
+
return (value,)
|
|
24
|
+
if isinstance(value, Iterable):
|
|
25
|
+
return tuple(str(item) for item in value)
|
|
26
|
+
raise TypeError("classes must be an iterable of strings or 'all'")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _normalise_banned(value: object) -> tuple[str, ...] | None:
|
|
30
|
+
if value is None:
|
|
31
|
+
return None
|
|
32
|
+
if isinstance(value, str):
|
|
33
|
+
return tuple(value)
|
|
34
|
+
if isinstance(value, Iterable):
|
|
35
|
+
return tuple(str(item) for item in value)
|
|
36
|
+
raise TypeError("banned_characters must be an iterable of strings")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _serialise_classes(
|
|
40
|
+
value: tuple[str, ...] | Literal["all"] | None,
|
|
41
|
+
) -> list[str] | Literal["all"] | None:
|
|
42
|
+
if value is None:
|
|
43
|
+
return None
|
|
44
|
+
if value == "all":
|
|
45
|
+
return "all"
|
|
46
|
+
return list(value)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _serialise_banned(value: tuple[str, ...] | None) -> list[str] | None:
|
|
50
|
+
if value is None:
|
|
51
|
+
return None
|
|
52
|
+
return list(value)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def swap_homoglyphs(
|
|
56
|
+
text: str,
|
|
57
|
+
rate: float | None = None,
|
|
58
|
+
classes: list[str] | Literal["all"] | None = None,
|
|
59
|
+
banned_characters: Collection[str] | None = None,
|
|
60
|
+
seed: int | None = None,
|
|
61
|
+
rng: random.Random | None = None,
|
|
62
|
+
) -> str:
|
|
63
|
+
"""Replace characters with visually confusable homoglyphs via the Rust engine."""
|
|
64
|
+
|
|
65
|
+
effective_rate = DEFAULT_MIM1C_RATE if rate is None else rate
|
|
66
|
+
|
|
67
|
+
normalised_classes = _normalise_classes(classes)
|
|
68
|
+
normalised_banned = _normalise_banned(banned_characters)
|
|
69
|
+
|
|
70
|
+
if normalised_classes is None:
|
|
71
|
+
payload_classes: list[str] | Literal["all"] | None = list(MIM1C_DEFAULT_CLASSES)
|
|
72
|
+
else:
|
|
73
|
+
payload_classes = _serialise_classes(normalised_classes)
|
|
74
|
+
payload_banned = _serialise_banned(normalised_banned)
|
|
75
|
+
|
|
76
|
+
return mim1c_rust(
|
|
77
|
+
text,
|
|
78
|
+
effective_rate,
|
|
79
|
+
payload_classes,
|
|
80
|
+
payload_banned,
|
|
81
|
+
resolve_seed(seed, rng),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Mim1c(Glitchling):
|
|
86
|
+
"""Glitchling that swaps characters for visually similar homoglyphs."""
|
|
87
|
+
|
|
88
|
+
flavor = (
|
|
89
|
+
"Breaks your parser by replacing some characters in strings with "
|
|
90
|
+
"doppelgangers. Don't worry, this text is clean. ;)"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
*,
|
|
96
|
+
rate: float | None = None,
|
|
97
|
+
classes: list[str] | Literal["all"] | None = None,
|
|
98
|
+
banned_characters: Collection[str] | None = None,
|
|
99
|
+
seed: int | None = None,
|
|
100
|
+
**kwargs: Any,
|
|
101
|
+
) -> None:
|
|
102
|
+
effective_rate = DEFAULT_MIM1C_RATE if rate is None else rate
|
|
103
|
+
normalised_classes = _normalise_classes(classes)
|
|
104
|
+
normalised_banned = _normalise_banned(banned_characters)
|
|
105
|
+
super().__init__(
|
|
106
|
+
name="Mim1c",
|
|
107
|
+
corruption_function=swap_homoglyphs,
|
|
108
|
+
scope=AttackWave.CHARACTER,
|
|
109
|
+
order=AttackOrder.LAST,
|
|
110
|
+
seed=seed,
|
|
111
|
+
rate=effective_rate,
|
|
112
|
+
classes=normalised_classes,
|
|
113
|
+
banned_characters=normalised_banned,
|
|
114
|
+
**kwargs,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def pipeline_operation(self) -> PipelineOperationPayload:
|
|
118
|
+
rate_value = self.kwargs.get("rate")
|
|
119
|
+
rate = DEFAULT_MIM1C_RATE if rate_value is None else float(rate_value)
|
|
120
|
+
|
|
121
|
+
descriptor: dict[str, object] = {"type": "mimic", "rate": rate}
|
|
122
|
+
|
|
123
|
+
classes = self.kwargs.get("classes")
|
|
124
|
+
serialised_classes = _serialise_classes(classes)
|
|
125
|
+
if serialised_classes is not None:
|
|
126
|
+
descriptor["classes"] = serialised_classes
|
|
127
|
+
|
|
128
|
+
banned = self.kwargs.get("banned_characters")
|
|
129
|
+
serialised_banned = _serialise_banned(banned)
|
|
130
|
+
if serialised_banned:
|
|
131
|
+
descriptor["banned_characters"] = serialised_banned
|
|
132
|
+
|
|
133
|
+
return cast(PipelineOperationPayload, descriptor)
|
|
134
|
+
|
|
135
|
+
def set_param(self, key: str, value: object) -> None:
|
|
136
|
+
if key == "classes":
|
|
137
|
+
super().set_param(key, _normalise_classes(value))
|
|
138
|
+
return
|
|
139
|
+
if key == "banned_characters":
|
|
140
|
+
super().set_param(key, _normalise_banned(value))
|
|
141
|
+
return
|
|
142
|
+
super().set_param(key, value)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
mim1c = Mim1c()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
__all__ = ["Mim1c", "mim1c", "swap_homoglyphs"]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Pedant glitchling integrating grammar evolutions with Rust acceleration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
from glitchlings.internal.rust_ffi import resolve_seed
|
|
9
|
+
|
|
10
|
+
from ..core import AttackOrder, AttackWave, Glitchling, PipelineOperationPayload
|
|
11
|
+
from .core import EVOLUTIONS, PedantBase, apply_pedant
|
|
12
|
+
from .stones import STONES, PedantStone
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _coerce_stone(value: Any) -> PedantStone:
|
|
16
|
+
"""Return a :class:`PedantStone` enum member for ``value``."""
|
|
17
|
+
|
|
18
|
+
return PedantStone.from_value(value)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def pedant_transform(
|
|
22
|
+
text: str,
|
|
23
|
+
*,
|
|
24
|
+
stone: PedantStone | str = PedantStone.COEURITE,
|
|
25
|
+
seed: int | None = None,
|
|
26
|
+
rng: random.Random | None = None,
|
|
27
|
+
) -> str:
|
|
28
|
+
"""Apply a pedant evolution to text."""
|
|
29
|
+
|
|
30
|
+
pedant_stone = _coerce_stone(stone)
|
|
31
|
+
if pedant_stone not in EVOLUTIONS:
|
|
32
|
+
raise ValueError(f"Unknown pedant stone: {stone!r}")
|
|
33
|
+
|
|
34
|
+
effective_seed = resolve_seed(seed, rng)
|
|
35
|
+
|
|
36
|
+
return apply_pedant(
|
|
37
|
+
text,
|
|
38
|
+
stone=pedant_stone,
|
|
39
|
+
seed=effective_seed,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _build_pipeline_descriptor(glitch: Glitchling) -> PipelineOperationPayload:
|
|
44
|
+
stone_value = glitch.kwargs.get("stone")
|
|
45
|
+
if stone_value is None:
|
|
46
|
+
message = "Pedant requires a stone to build the pipeline descriptor"
|
|
47
|
+
raise RuntimeError(message)
|
|
48
|
+
|
|
49
|
+
pedant_stone = _coerce_stone(stone_value)
|
|
50
|
+
|
|
51
|
+
return cast(
|
|
52
|
+
PipelineOperationPayload,
|
|
53
|
+
{"type": "pedant", "stone": pedant_stone.label},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Pedant(Glitchling):
|
|
58
|
+
"""Glitchling that deterministically applies pedant evolutions."""
|
|
59
|
+
|
|
60
|
+
_param_aliases = {
|
|
61
|
+
"form": "stone",
|
|
62
|
+
"stone_name": "stone",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
stone: PedantStone | str = PedantStone.COEURITE,
|
|
69
|
+
seed: int | None = None,
|
|
70
|
+
**kwargs: Any,
|
|
71
|
+
) -> None:
|
|
72
|
+
super().__init__(
|
|
73
|
+
name="Pedant",
|
|
74
|
+
corruption_function=pedant_transform,
|
|
75
|
+
scope=AttackWave.WORD,
|
|
76
|
+
order=AttackOrder.LATE,
|
|
77
|
+
seed=seed,
|
|
78
|
+
pipeline_operation=_build_pipeline_descriptor,
|
|
79
|
+
stone=_coerce_stone(stone),
|
|
80
|
+
**kwargs,
|
|
81
|
+
)
|
|
82
|
+
if seed is not None:
|
|
83
|
+
self.set_param("seed", int(seed))
|
|
84
|
+
|
|
85
|
+
def set_param(self, key: str, value: object) -> None:
|
|
86
|
+
if key in {"stone", "form", "stone_name"}:
|
|
87
|
+
super().set_param(key, _coerce_stone(value))
|
|
88
|
+
return
|
|
89
|
+
super().set_param(key, value)
|
|
90
|
+
|
|
91
|
+
def reset_rng(self, seed: int | None = None) -> None:
|
|
92
|
+
super().reset_rng(seed)
|
|
93
|
+
if self.seed is None:
|
|
94
|
+
self.kwargs.pop("seed", None)
|
|
95
|
+
return
|
|
96
|
+
self.kwargs["seed"] = int(self.seed)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
pedant = Pedant()
|
|
100
|
+
|
|
101
|
+
__all__ = [
|
|
102
|
+
"PedantBase",
|
|
103
|
+
"Pedant",
|
|
104
|
+
"pedant",
|
|
105
|
+
"pedant_transform",
|
|
106
|
+
"EVOLUTIONS",
|
|
107
|
+
"STONES",
|
|
108
|
+
"PedantStone",
|
|
109
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Core classes for the pedant evolution chain backed by the Rust pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Type
|
|
6
|
+
|
|
7
|
+
from glitchlings.internal.rust_ffi import pedant_rust
|
|
8
|
+
|
|
9
|
+
from ..core import Gaggle
|
|
10
|
+
from .stones import PedantStone
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def apply_pedant(
|
|
14
|
+
text: str,
|
|
15
|
+
*,
|
|
16
|
+
stone: PedantStone,
|
|
17
|
+
seed: int,
|
|
18
|
+
) -> str:
|
|
19
|
+
"""Apply a pedant transformation via the Rust extension."""
|
|
20
|
+
|
|
21
|
+
return pedant_rust(
|
|
22
|
+
text,
|
|
23
|
+
stone=stone.label,
|
|
24
|
+
seed=int(seed),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PedantEvolution:
|
|
29
|
+
"""Concrete pedant form that delegates to the Rust implementation."""
|
|
30
|
+
|
|
31
|
+
stone: PedantStone
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
seed: int,
|
|
36
|
+
*,
|
|
37
|
+
stone: PedantStone | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
resolved_stone = stone or getattr(self, "stone", None)
|
|
40
|
+
if resolved_stone is None: # pragma: no cover - defensive guard
|
|
41
|
+
raise ValueError("PedantEvolution requires a PedantStone")
|
|
42
|
+
self.seed = int(seed)
|
|
43
|
+
self.stone = resolved_stone
|
|
44
|
+
|
|
45
|
+
def move(self, text: str) -> str:
|
|
46
|
+
result = apply_pedant(text, stone=self.stone, seed=self.seed)
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PedantBase:
|
|
51
|
+
"""Base pedant capable of evolving into specialised grammar forms."""
|
|
52
|
+
|
|
53
|
+
name: str = "Pedant"
|
|
54
|
+
type: str = "Normal"
|
|
55
|
+
flavor: str = "A novice grammarian waiting to evolve."
|
|
56
|
+
|
|
57
|
+
def __init__(self, seed: int, *, root_seed: int | None = None) -> None:
|
|
58
|
+
self.seed = int(seed)
|
|
59
|
+
self.root_seed = int(seed if root_seed is None else root_seed)
|
|
60
|
+
|
|
61
|
+
def evolve(self, stone: PedantStone | str) -> PedantEvolution:
|
|
62
|
+
pedant_stone = PedantStone.from_value(stone)
|
|
63
|
+
form_cls = EVOLUTIONS.get(pedant_stone)
|
|
64
|
+
if form_cls is None: # pragma: no cover - sanity guard
|
|
65
|
+
raise KeyError(f"Unknown stone: {stone}")
|
|
66
|
+
derived_seed = Gaggle.derive_seed(self.root_seed, pedant_stone.label, 0)
|
|
67
|
+
return form_cls(seed=int(derived_seed))
|
|
68
|
+
|
|
69
|
+
def move(self, text: str) -> str:
|
|
70
|
+
return text
|
|
71
|
+
|
|
72
|
+
def __repr__(self) -> str: # pragma: no cover - debugging helper
|
|
73
|
+
return f"<{self.__class__.__name__} seed={self.seed} type={self.type}>"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
EVOLUTIONS: Dict[PedantStone, Type[PedantEvolution]] = {}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
try: # pragma: no cover - import resolution occurs at runtime
|
|
80
|
+
from .forms import (
|
|
81
|
+
Aetheria,
|
|
82
|
+
Apostrofae,
|
|
83
|
+
Commama,
|
|
84
|
+
Correctopus,
|
|
85
|
+
Fewerling,
|
|
86
|
+
Kiloa,
|
|
87
|
+
Subjunic,
|
|
88
|
+
Whomst,
|
|
89
|
+
)
|
|
90
|
+
except ImportError: # pragma: no cover - partial imports during type checking
|
|
91
|
+
pass
|
|
92
|
+
else:
|
|
93
|
+
EVOLUTIONS = {
|
|
94
|
+
PedantStone.WHOM: Whomst,
|
|
95
|
+
PedantStone.FEWERITE: Fewerling,
|
|
96
|
+
PedantStone.COEURITE: Aetheria,
|
|
97
|
+
PedantStone.CURLITE: Apostrofae,
|
|
98
|
+
PedantStone.SUBJUNCTITE: Subjunic,
|
|
99
|
+
PedantStone.OXFORDIUM: Commama,
|
|
100
|
+
PedantStone.ORTHOGONITE: Correctopus,
|
|
101
|
+
PedantStone.METRICITE: Kiloa,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = ["PedantBase", "PedantEvolution", "EVOLUTIONS", "apply_pedant"]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Pedant evolution forms delegating to the Rust-backed core."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .core import PedantEvolution
|
|
6
|
+
from .stones import PedantStone
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Whomst(PedantEvolution):
|
|
10
|
+
stone = PedantStone.WHOM
|
|
11
|
+
name = "Whomst"
|
|
12
|
+
type = "Ghost"
|
|
13
|
+
flavor = "Insists upon objective-case precision."
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Fewerling(PedantEvolution):
|
|
17
|
+
stone = PedantStone.FEWERITE
|
|
18
|
+
name = "Fewerling"
|
|
19
|
+
type = "Fairy"
|
|
20
|
+
flavor = "Counts only countable nouns."
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Aetheria(PedantEvolution):
|
|
24
|
+
stone = PedantStone.COEURITE
|
|
25
|
+
name = "Aetheria"
|
|
26
|
+
type = "Psychic"
|
|
27
|
+
flavor = "Resurrects archaic ligatures and diacritics."
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Apostrofae(PedantEvolution):
|
|
31
|
+
stone = PedantStone.CURLITE
|
|
32
|
+
name = "Apostrofae"
|
|
33
|
+
type = "Fairy"
|
|
34
|
+
flavor = "Curves quotes into typeset perfection."
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Subjunic(PedantEvolution):
|
|
38
|
+
stone = PedantStone.SUBJUNCTITE
|
|
39
|
+
name = "Subjunic"
|
|
40
|
+
type = "Psychic"
|
|
41
|
+
flavor = "Corrects the subjunctive wherever it can."
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Commama(PedantEvolution):
|
|
45
|
+
stone = PedantStone.OXFORDIUM
|
|
46
|
+
name = "Commama"
|
|
47
|
+
type = "Steel"
|
|
48
|
+
flavor = "Oxonian hero of the list."
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Kiloa(PedantEvolution):
|
|
52
|
+
stone = PedantStone.METRICITE
|
|
53
|
+
name = "Kiloa"
|
|
54
|
+
type = "Electric"
|
|
55
|
+
flavor = "Measures the world in rational units."
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Correctopus(PedantEvolution):
|
|
59
|
+
stone = PedantStone.ORTHOGONITE
|
|
60
|
+
name = "Correctopus"
|
|
61
|
+
type = "Dragon"
|
|
62
|
+
flavor = "The final editor, breathing blue ink."
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
"Whomst",
|
|
67
|
+
"Fewerling",
|
|
68
|
+
"Aetheria",
|
|
69
|
+
"Apostrofae",
|
|
70
|
+
"Subjunic",
|
|
71
|
+
"Commama",
|
|
72
|
+
"Kiloa",
|
|
73
|
+
"Correctopus",
|
|
74
|
+
]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Evolution stones recognised by the pedant."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class Stone:
|
|
11
|
+
"""Descriptor for an evolution stone."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
type: str
|
|
15
|
+
effect: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PedantStone(Enum):
|
|
19
|
+
"""Enumeration of evolution stones available to the pedant."""
|
|
20
|
+
|
|
21
|
+
WHOM = Stone("Whom Stone", "Ghost", "Encourages object-pronoun precision.")
|
|
22
|
+
FEWERITE = Stone("Fewerite", "Fairy", "Obsesses over countable quantities.")
|
|
23
|
+
COEURITE = Stone("Coeurite", "Psychic", "Restores archaic ligatures to modern words.")
|
|
24
|
+
CURLITE = Stone(
|
|
25
|
+
"Curlite",
|
|
26
|
+
"Fairy",
|
|
27
|
+
"Coaches punctuation to embrace typographic curls.",
|
|
28
|
+
)
|
|
29
|
+
SUBJUNCTITE = Stone("Subjunctite", "Psychic", "Demands contrary-to-fact phrasing.")
|
|
30
|
+
OXFORDIUM = Stone("Oxfordium", "Steel", "Polishes serial comma usage.")
|
|
31
|
+
ORTHOGONITE = Stone("Orthogonite", "Dragon", "Unlocks the legendary Correctopus.")
|
|
32
|
+
METRICITE = Stone("Metricite", "Electric", "Compels metrication of measures.")
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def descriptor(self) -> Stone:
|
|
36
|
+
"""Return the metadata describing this stone."""
|
|
37
|
+
|
|
38
|
+
return self.value
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def label(self) -> str:
|
|
42
|
+
"""Return the display name for this stone."""
|
|
43
|
+
|
|
44
|
+
return self.value.name
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str: # pragma: no cover - convenience for reprs/CLI echo
|
|
47
|
+
return self.label
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_value(cls, value: object) -> "PedantStone":
|
|
51
|
+
"""Normalise ``value`` into a :class:`PedantStone` member."""
|
|
52
|
+
|
|
53
|
+
if isinstance(value, cls):
|
|
54
|
+
return value
|
|
55
|
+
if isinstance(value, Stone):
|
|
56
|
+
for member in cls:
|
|
57
|
+
if member.value == value:
|
|
58
|
+
return member
|
|
59
|
+
msg = f"Unknown pedant stone descriptor: {value!r}"
|
|
60
|
+
raise ValueError(msg)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
return _STONE_BY_NAME[str(value)]
|
|
64
|
+
except KeyError as exc:
|
|
65
|
+
raise ValueError(f"Unknown pedant stone: {value!r}") from exc
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
_STONE_BY_NAME: dict[str, PedantStone] = {stone.value.name: stone for stone in PedantStone}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
STONES: dict[str, Stone] = {stone.label: stone.descriptor for stone in PedantStone}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__all__ = ["Stone", "PedantStone", "STONES"]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from typing import Any, cast
|
|
3
|
+
|
|
4
|
+
from glitchlings.constants import DEFAULT_REDACTYL_CHAR, DEFAULT_REDACTYL_RATE
|
|
5
|
+
from glitchlings.internal.rust_ffi import redact_words_rust, resolve_seed
|
|
6
|
+
|
|
7
|
+
from .core import AttackWave, Glitchling, PipelineOperationPayload
|
|
8
|
+
|
|
9
|
+
# Backwards compatibility alias
|
|
10
|
+
FULL_BLOCK = DEFAULT_REDACTYL_CHAR
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def redact_words(
|
|
14
|
+
text: str,
|
|
15
|
+
replacement_char: str | None = DEFAULT_REDACTYL_CHAR,
|
|
16
|
+
rate: float | None = None,
|
|
17
|
+
merge_adjacent: bool | None = False,
|
|
18
|
+
seed: int = 151,
|
|
19
|
+
rng: random.Random | None = None,
|
|
20
|
+
*,
|
|
21
|
+
unweighted: bool = False,
|
|
22
|
+
) -> str:
|
|
23
|
+
"""Redact random words by replacing their characters."""
|
|
24
|
+
effective_rate = DEFAULT_REDACTYL_RATE if rate is None else rate
|
|
25
|
+
|
|
26
|
+
replacement = DEFAULT_REDACTYL_CHAR if replacement_char is None else str(replacement_char)
|
|
27
|
+
merge = False if merge_adjacent is None else bool(merge_adjacent)
|
|
28
|
+
|
|
29
|
+
clamped_rate = max(0.0, min(effective_rate, 1.0))
|
|
30
|
+
unweighted_flag = bool(unweighted)
|
|
31
|
+
|
|
32
|
+
return redact_words_rust(
|
|
33
|
+
text,
|
|
34
|
+
replacement,
|
|
35
|
+
clamped_rate,
|
|
36
|
+
merge,
|
|
37
|
+
unweighted_flag,
|
|
38
|
+
resolve_seed(seed, rng),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Redactyl(Glitchling):
|
|
43
|
+
"""Glitchling that redacts words with block characters."""
|
|
44
|
+
|
|
45
|
+
flavor = "Some things are better left ████████."
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
replacement_char: str = DEFAULT_REDACTYL_CHAR,
|
|
51
|
+
rate: float | None = None,
|
|
52
|
+
merge_adjacent: bool = False,
|
|
53
|
+
seed: int = 151,
|
|
54
|
+
unweighted: bool = False,
|
|
55
|
+
**kwargs: Any,
|
|
56
|
+
) -> None:
|
|
57
|
+
effective_rate = DEFAULT_REDACTYL_RATE if rate is None else rate
|
|
58
|
+
super().__init__(
|
|
59
|
+
name="Redactyl",
|
|
60
|
+
corruption_function=redact_words,
|
|
61
|
+
scope=AttackWave.WORD,
|
|
62
|
+
seed=seed,
|
|
63
|
+
replacement_char=replacement_char,
|
|
64
|
+
rate=effective_rate,
|
|
65
|
+
merge_adjacent=merge_adjacent,
|
|
66
|
+
unweighted=unweighted,
|
|
67
|
+
**kwargs,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def pipeline_operation(self) -> PipelineOperationPayload:
|
|
71
|
+
replacement_char_value = self.kwargs.get("replacement_char", DEFAULT_REDACTYL_CHAR)
|
|
72
|
+
rate_value = self.kwargs.get("rate", DEFAULT_REDACTYL_RATE)
|
|
73
|
+
merge_value = self.kwargs.get("merge_adjacent", False)
|
|
74
|
+
|
|
75
|
+
replacement_char = str(
|
|
76
|
+
DEFAULT_REDACTYL_CHAR if replacement_char_value is None else replacement_char_value
|
|
77
|
+
)
|
|
78
|
+
rate = float(DEFAULT_REDACTYL_RATE if rate_value is None else rate_value)
|
|
79
|
+
merge_adjacent = bool(merge_value)
|
|
80
|
+
unweighted = bool(self.kwargs.get("unweighted", False))
|
|
81
|
+
|
|
82
|
+
return cast(
|
|
83
|
+
PipelineOperationPayload,
|
|
84
|
+
{
|
|
85
|
+
"type": "redact",
|
|
86
|
+
"replacement_char": replacement_char,
|
|
87
|
+
"rate": rate,
|
|
88
|
+
"merge_adjacent": merge_adjacent,
|
|
89
|
+
"unweighted": unweighted,
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
redactyl = Redactyl()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = ["Redactyl", "redactyl", "redact_words"]
|