Coverage for src / hodoku / core / scoring.py: 100%
21 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-21 08:35 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-21 08:35 +0000
1"""Scoring configuration — mirrors Options.solverSteps[] and difficultyLevels[] in Java.
3Each StepConfig records the solver priority (index), base score per application,
4difficulty level, and enabled flags for a technique.
6The Python enum splits some Java SolutionTypes into subtypes (e.g. SIMPLE_COLORS →
7TRAP/WRAP, NICE_LOOP → Continuous/Discontinuous/AIC). Those subtypes share the same
8StepConfig via the STEP_CONFIG alias table at the bottom.
9"""
11from __future__ import annotations
13from dataclasses import dataclass
15from hodoku.core.types import DifficultyType, SolutionCategory, SolutionType
17_D = DifficultyType
18_C = SolutionCategory
19_S = SolutionType
22@dataclass(frozen=True)
23class StepConfig:
24 index: int # solver priority (lower = tried first)
25 solution_type: SolutionType
26 level: DifficultyType # difficulty this technique belongs to
27 category: SolutionCategory
28 base_score: int # points added per application
29 enabled: bool # used in the solve loop?
30 all_steps_enabled: bool # searched in find-all-steps mode?
33# ---------------------------------------------------------------------------
34# Difficulty level score thresholds
35# Puzzle difficulty = max level of all techniques used.
36# These thresholds are used by the generator to filter puzzles by difficulty.
37# ---------------------------------------------------------------------------
39DIFFICULTY_MAX_SCORE: dict[DifficultyType, int] = {
40 _D.INCOMPLETE: 0,
41 _D.EASY: 800,
42 _D.MEDIUM: 1000,
43 _D.HARD: 1600,
44 _D.UNFAIR: 1800,
45 _D.EXTREME: 2**31 - 1,
46}
48# ---------------------------------------------------------------------------
49# Full step table — transcribed from Options.solverSteps[] in Options.java.
50# Rows appear in Java source order; SOLVER_STEPS below sorts by index.
51# ---------------------------------------------------------------------------
53DEFAULT_STEPS: tuple[StepConfig, ...] = (
54 # Sentinels
55 StepConfig(2**31 - 2, _S.INCOMPLETE, _D.INCOMPLETE, _C.LAST_RESORT, 0, False, False),
56 StepConfig(2**31 - 1, _S.GIVE_UP, _D.EXTREME, _C.LAST_RESORT, 20000, True, False),
57 # Singles
58 StepConfig(100, _S.FULL_HOUSE, _D.EASY, _C.SINGLES, 4, True, True),
59 StepConfig(200, _S.NAKED_SINGLE, _D.EASY, _C.SINGLES, 4, True, True),
60 StepConfig(300, _S.HIDDEN_SINGLE, _D.EASY, _C.SINGLES, 14, True, True),
61 # Intersections
62 StepConfig(1000, _S.LOCKED_PAIR, _D.MEDIUM, _C.INTERSECTIONS, 40, True, True),
63 StepConfig(1100, _S.LOCKED_TRIPLE, _D.MEDIUM, _C.INTERSECTIONS, 60, True, True),
64 StepConfig(1200, _S.LOCKED_CANDIDATES_1, _D.MEDIUM, _C.INTERSECTIONS, 50, True, True),
65 StepConfig(1210, _S.LOCKED_CANDIDATES_2, _D.MEDIUM, _C.INTERSECTIONS, 50, True, True),
66 # Subsets
67 StepConfig(1300, _S.NAKED_PAIR, _D.MEDIUM, _C.SUBSETS, 60, True, True),
68 StepConfig(1400, _S.NAKED_TRIPLE, _D.MEDIUM, _C.SUBSETS, 80, True, True),
69 StepConfig(1500, _S.HIDDEN_PAIR, _D.MEDIUM, _C.SUBSETS, 70, True, True),
70 StepConfig(1600, _S.HIDDEN_TRIPLE, _D.MEDIUM, _C.SUBSETS, 100, True, True),
71 StepConfig(2000, _S.NAKED_QUADRUPLE, _D.HARD, _C.SUBSETS, 120, True, True),
72 StepConfig(2100, _S.HIDDEN_QUADRUPLE, _D.HARD, _C.SUBSETS, 150, True, True),
73 # Basic fish
74 StepConfig(2200, _S.X_WING, _D.HARD, _C.BASIC_FISH, 140, True, False),
75 StepConfig(2300, _S.SWORDFISH, _D.HARD, _C.BASIC_FISH, 150, True, False),
76 StepConfig(2400, _S.JELLYFISH, _D.HARD, _C.BASIC_FISH, 160, True, False),
77 StepConfig(2500, _S.SQUIRMBAG, _D.UNFAIR, _C.BASIC_FISH, 470, False, False),
78 StepConfig(2600, _S.WHALE, _D.UNFAIR, _C.BASIC_FISH, 470, False, False),
79 StepConfig(2700, _S.LEVIATHAN, _D.UNFAIR, _C.BASIC_FISH, 470, False, False),
80 # Chains (basic)
81 StepConfig(2800, _S.REMOTE_PAIR, _D.HARD, _C.CHAINS_AND_LOOPS, 110, True, True),
82 # Uniqueness
83 StepConfig(2900, _S.BUG_PLUS_1, _D.HARD, _C.UNIQUENESS, 100, True, True),
84 # Single digit patterns
85 StepConfig(3000, _S.SKYSCRAPER, _D.HARD, _C.SINGLE_DIGIT_PATTERNS, 130, True, True),
86 StepConfig(3100, _S.TWO_STRING_KITE, _D.HARD, _C.SINGLE_DIGIT_PATTERNS, 150, True, True),
87 StepConfig(3120, _S.TURBOT_FISH, _D.HARD, _C.SINGLE_DIGIT_PATTERNS, 120, True, True),
88 StepConfig(3170, _S.EMPTY_RECTANGLE, _D.HARD, _C.SINGLE_DIGIT_PATTERNS, 120, True, True),
89 # Wings
90 StepConfig(3200, _S.W_WING, _D.HARD, _C.WINGS, 150, True, True),
91 StepConfig(3300, _S.XY_WING, _D.HARD, _C.WINGS, 160, True, True),
92 StepConfig(3400, _S.XYZ_WING, _D.HARD, _C.WINGS, 180, True, True),
93 # Uniqueness tests
94 StepConfig(3500, _S.UNIQUENESS_1, _D.HARD, _C.UNIQUENESS, 100, True, True),
95 StepConfig(3600, _S.UNIQUENESS_2, _D.HARD, _C.UNIQUENESS, 100, True, True),
96 StepConfig(3700, _S.UNIQUENESS_3, _D.HARD, _C.UNIQUENESS, 100, True, True),
97 StepConfig(3800, _S.UNIQUENESS_4, _D.HARD, _C.UNIQUENESS, 100, True, True),
98 StepConfig(3900, _S.UNIQUENESS_5, _D.HARD, _C.UNIQUENESS, 100, True, True),
99 StepConfig(4000, _S.UNIQUENESS_6, _D.HARD, _C.UNIQUENESS, 100, True, True),
100 StepConfig(4010, _S.HIDDEN_RECTANGLE, _D.HARD, _C.UNIQUENESS, 100, True, True),
101 StepConfig(4020, _S.AVOIDABLE_RECTANGLE_1, _D.HARD, _C.UNIQUENESS, 100, True, True),
102 StepConfig(4030, _S.AVOIDABLE_RECTANGLE_2, _D.HARD, _C.UNIQUENESS, 100, True, True),
103 # Finned basic fish
104 StepConfig(4100, _S.FINNED_X_WING, _D.HARD, _C.FINNED_BASIC_FISH, 130, True, False),
105 StepConfig(4200, _S.SASHIMI_X_WING, _D.HARD, _C.FINNED_BASIC_FISH, 150, True, False),
106 StepConfig(4300, _S.FINNED_SWORDFISH, _D.UNFAIR, _C.FINNED_BASIC_FISH, 200, True, False),
107 StepConfig(4400, _S.SASHIMI_SWORDFISH, _D.UNFAIR, _C.FINNED_BASIC_FISH, 240, True, False),
108 StepConfig(4500, _S.FINNED_JELLYFISH, _D.UNFAIR, _C.FINNED_BASIC_FISH, 250, True, False),
109 StepConfig(4600, _S.SASHIMI_JELLYFISH, _D.UNFAIR, _C.FINNED_BASIC_FISH, 260, True, False),
110 StepConfig(4700, _S.FINNED_SQUIRMBAG, _D.UNFAIR, _C.FINNED_BASIC_FISH, 470, False, False),
111 StepConfig(4800, _S.SASHIMI_SQUIRMBAG, _D.UNFAIR, _C.FINNED_BASIC_FISH, 470, False, False),
112 StepConfig(4900, _S.FINNED_WHALE, _D.UNFAIR, _C.FINNED_BASIC_FISH, 470, False, False),
113 StepConfig(5000, _S.SASHIMI_WHALE, _D.UNFAIR, _C.FINNED_BASIC_FISH, 470, False, False),
114 StepConfig(5100, _S.FINNED_LEVIATHAN, _D.UNFAIR, _C.FINNED_BASIC_FISH, 470, False, False),
115 StepConfig(5200, _S.SASHIMI_LEVIATHAN, _D.UNFAIR, _C.FINNED_BASIC_FISH, 470, False, False),
116 # Miscellaneous
117 StepConfig(5300, _S.SUE_DE_COQ, _D.UNFAIR, _C.MISCELLANEOUS, 250, True, True),
118 # Coloring (Java SIMPLE_COLORS / MULTI_COLORS; Python splits into subtypes)
119 StepConfig(5330, _S.SIMPLE_COLORS_TRAP, _D.HARD, _C.COLORING, 150, True, True),
120 StepConfig(5360, _S.MULTI_COLORS_1, _D.HARD, _C.COLORING, 200, True, True),
121 # Chains and loops
122 StepConfig(5400, _S.X_CHAIN, _D.UNFAIR, _C.CHAINS_AND_LOOPS, 260, True, True),
123 StepConfig(5500, _S.XY_CHAIN, _D.UNFAIR, _C.CHAINS_AND_LOOPS, 260, True, True),
124 StepConfig(5600, _S.CONTINUOUS_NICE_LOOP, _D.UNFAIR, _C.CHAINS_AND_LOOPS, 280, True, True),
125 StepConfig(5650, _S.GROUPED_CONTINUOUS_NICE_LOOP, _D.UNFAIR, _C.CHAINS_AND_LOOPS, 300, True, True),
126 # Almost locked sets
127 StepConfig(5700, _S.ALS_XZ, _D.UNFAIR, _C.ALMOST_LOCKED_SETS, 300, True, True),
128 StepConfig(5800, _S.ALS_XY_WING, _D.UNFAIR, _C.ALMOST_LOCKED_SETS, 320, True, True),
129 StepConfig(5900, _S.ALS_XY_CHAIN, _D.UNFAIR, _C.ALMOST_LOCKED_SETS, 340, True, True),
130 StepConfig(6000, _S.DEATH_BLOSSOM, _D.UNFAIR, _C.ALMOST_LOCKED_SETS, 360, False, True),
131 # Franken fish
132 StepConfig(6100, _S.FRANKEN_X_WING, _D.UNFAIR, _C.FRANKEN_FISH, 300, True, False),
133 StepConfig(6200, _S.FRANKEN_SWORDFISH, _D.UNFAIR, _C.FRANKEN_FISH, 350, True, False),
134 StepConfig(6300, _S.FRANKEN_JELLYFISH, _D.UNFAIR, _C.FRANKEN_FISH, 370, False, False),
135 StepConfig(6400, _S.FRANKEN_SQUIRMBAG, _D.EXTREME, _C.FRANKEN_FISH, 470, False, False),
136 StepConfig(6500, _S.FRANKEN_WHALE, _D.EXTREME, _C.FRANKEN_FISH, 470, False, False),
137 StepConfig(6600, _S.FRANKEN_LEVIATHAN, _D.EXTREME, _C.FRANKEN_FISH, 470, False, False),
138 # Finned franken fish
139 StepConfig(6700, _S.FINNED_FRANKEN_X_WING, _D.UNFAIR, _C.FINNED_FRANKEN_FISH, 390, True, False),
140 StepConfig(6800, _S.FINNED_FRANKEN_SWORDFISH, _D.UNFAIR, _C.FINNED_FRANKEN_FISH, 410, True, False),
141 StepConfig(6900, _S.FINNED_FRANKEN_JELLYFISH, _D.UNFAIR, _C.FINNED_FRANKEN_FISH, 430, False, False),
142 StepConfig(7000, _S.FINNED_FRANKEN_SQUIRMBAG, _D.EXTREME, _C.FINNED_FRANKEN_FISH, 470, False, False),
143 StepConfig(7100, _S.FINNED_FRANKEN_WHALE, _D.EXTREME, _C.FINNED_FRANKEN_FISH, 470, False, False),
144 StepConfig(7200, _S.FINNED_FRANKEN_LEVIATHAN, _D.EXTREME, _C.FINNED_FRANKEN_FISH, 470, False, False),
145 # Mutant fish
146 StepConfig(7300, _S.MUTANT_X_WING, _D.EXTREME, _C.MUTANT_FISH, 450, False, False),
147 StepConfig(7400, _S.MUTANT_SWORDFISH, _D.EXTREME, _C.MUTANT_FISH, 450, False, False),
148 StepConfig(7500, _S.MUTANT_JELLYFISH, _D.EXTREME, _C.MUTANT_FISH, 450, False, False),
149 StepConfig(7600, _S.MUTANT_SQUIRMBAG, _D.EXTREME, _C.MUTANT_FISH, 470, False, False),
150 StepConfig(7700, _S.MUTANT_WHALE, _D.EXTREME, _C.MUTANT_FISH, 470, False, False),
151 StepConfig(7800, _S.MUTANT_LEVIATHAN, _D.EXTREME, _C.MUTANT_FISH, 470, False, False),
152 # Finned mutant fish
153 StepConfig(7900, _S.FINNED_MUTANT_X_WING, _D.EXTREME, _C.FINNED_MUTANT_FISH, 470, False, False),
154 StepConfig(8000, _S.FINNED_MUTANT_SWORDFISH, _D.EXTREME, _C.FINNED_MUTANT_FISH, 470, False, False),
155 StepConfig(8100, _S.FINNED_MUTANT_JELLYFISH, _D.EXTREME, _C.FINNED_MUTANT_FISH, 470, False, False),
156 StepConfig(8200, _S.FINNED_MUTANT_SQUIRMBAG, _D.EXTREME, _C.FINNED_MUTANT_FISH, 470, False, False),
157 StepConfig(8300, _S.FINNED_MUTANT_WHALE, _D.EXTREME, _C.FINNED_MUTANT_FISH, 470, False, False),
158 StepConfig(8400, _S.FINNED_MUTANT_LEVIATHAN, _D.EXTREME, _C.FINNED_MUTANT_FISH, 470, False, False),
159 # Last resort
160 StepConfig(8450, _S.KRAKEN_FISH_TYPE_1, _D.EXTREME, _C.LAST_RESORT, 500, False, False),
161 StepConfig(8500, _S.FORCING_CHAIN_CONTRADICTION, _D.EXTREME, _C.LAST_RESORT, 500, True, False),
162 StepConfig(8600, _S.FORCING_NET_CONTRADICTION, _D.EXTREME, _C.LAST_RESORT, 700, True, False),
163 StepConfig(8700, _S.TEMPLATE_SET, _D.EXTREME, _C.LAST_RESORT, 10000, False, False),
164 StepConfig(8800, _S.TEMPLATE_DEL, _D.EXTREME, _C.LAST_RESORT, 10000, False, False),
165 StepConfig(8900, _S.BRUTE_FORCE, _D.EXTREME, _C.LAST_RESORT, 10000, True, False),
166)
168# ---------------------------------------------------------------------------
169# Derived tables
170# ---------------------------------------------------------------------------
172# Ordered sequence used by the solve loop — enabled steps only, sorted by index.
173SOLVER_STEPS: tuple[StepConfig, ...] = tuple(
174 sorted((s for s in DEFAULT_STEPS if s.enabled), key=lambda s: s.index)
175)
177# Fast lookup by SolutionType. Subtypes that Java maps to a single StepConfig
178# are aliased below to their primary variant's config.
179STEP_CONFIG: dict[SolutionType, StepConfig] = {
180 s.solution_type: s for s in DEFAULT_STEPS
181}
183# Aliases: Python splits these Java types into named subtypes.
184# All subtypes share the primary variant's score and level.
185STEP_CONFIG.update({
186 # SIMPLE_COLORS → TRAP (primary, index 5330) + WRAP
187 _S.SIMPLE_COLORS_WRAP: STEP_CONFIG[_S.SIMPLE_COLORS_TRAP],
188 # MULTI_COLORS → 1 (primary, index 5360) + 2
189 _S.MULTI_COLORS_2: STEP_CONFIG[_S.MULTI_COLORS_1],
190 # NICE_LOOP → Continuous (primary, index 5600) + Discontinuous + AIC
191 _S.DISCONTINUOUS_NICE_LOOP: STEP_CONFIG[_S.CONTINUOUS_NICE_LOOP],
192 _S.AIC: STEP_CONFIG[_S.CONTINUOUS_NICE_LOOP],
193 # GROUPED_NICE_LOOP → Grouped Continuous (primary, index 5650) + others
194 _S.GROUPED_DISCONTINUOUS_NICE_LOOP: STEP_CONFIG[_S.GROUPED_CONTINUOUS_NICE_LOOP],
195 _S.GROUPED_AIC: STEP_CONFIG[_S.GROUPED_CONTINUOUS_NICE_LOOP],
196 # FORCING_CHAIN → Contradiction (primary, index 8500) + Verity
197 _S.FORCING_CHAIN_VERITY: STEP_CONFIG[_S.FORCING_CHAIN_CONTRADICTION],
198 # FORCING_NET → Contradiction (primary, index 8600) + Verity
199 _S.FORCING_NET_VERITY: STEP_CONFIG[_S.FORCING_NET_CONTRADICTION],
200 # KRAKEN_FISH → Type 1 (primary, index 8450) + Type 2
201 _S.KRAKEN_FISH_TYPE_2: STEP_CONFIG[_S.KRAKEN_FISH_TYPE_1],
202 # DUAL_* — not in Java StepConfig; treat as same score as their parent
203 _S.DUAL_TWO_STRING_KITE: STEP_CONFIG[_S.TWO_STRING_KITE],
204 _S.DUAL_EMPTY_RECTANGLE: STEP_CONFIG[_S.EMPTY_RECTANGLE],
205})
207# Score thresholds from Options.DEFAULT_DIFFICULTY_LEVELS in Java.
208# If total score exceeds a level's max, the puzzle is bumped to the next level.
209# Mirrors: while (score > level.getMaxScore()) level = nextLevel;
210DIFFICULTY_MAX_SCORE: dict[DifficultyType, int] = {
211 DifficultyType.EASY: 800,
212 DifficultyType.MEDIUM: 1000,
213 DifficultyType.HARD: 1600,
214 DifficultyType.UNFAIR: 1800,
215 DifficultyType.EXTREME: 2**31 - 1, # Integer.MAX_VALUE
216}