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

1"""Scoring configuration — mirrors Options.solverSteps[] and difficultyLevels[] in Java. 

2 

3Each StepConfig records the solver priority (index), base score per application, 

4difficulty level, and enabled flags for a technique. 

5 

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""" 

10 

11from __future__ import annotations 

12 

13from dataclasses import dataclass 

14 

15from hodoku.core.types import DifficultyType, SolutionCategory, SolutionType 

16 

17_D = DifficultyType 

18_C = SolutionCategory 

19_S = SolutionType 

20 

21 

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? 

31 

32 

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# --------------------------------------------------------------------------- 

38 

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} 

47 

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# --------------------------------------------------------------------------- 

52 

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) 

167 

168# --------------------------------------------------------------------------- 

169# Derived tables 

170# --------------------------------------------------------------------------- 

171 

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) 

176 

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} 

182 

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}) 

206 

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}