Coverage for src/seqrule/rulesets/eleusis.py: 35%

208 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-27 10:39 -0600

1""" 

2Classic Eleusis card game rules. 

3 

4This module implements both classic and creative rules for the Eleusis card game, 

5showcasing a wide range of pattern matching capabilities: 

6 

7Classic Rules: 

8- Red/black alternation 

9- Suit cycles 

10- Fixed patterns 

11- Odd/even numbers 

12- Range-based rules 

13- Increment patterns 

14 

15Creative Rules: 

16- Mathematical patterns 

17- Multi-card relationships 

18- Historical patterns 

19- Card properties combinations 

20- Complex sequences 

21- Meta-rules 

22""" 

23 

24import math 

25from typing import Dict, List, Sequence 

26 

27from ..core import AbstractObject 

28from ..dsl import DSLRule 

29 

30 

31class Card(AbstractObject): 

32 """A playing card with color, suit, and number properties.""" 

33 

34 def __init__(self, color: str, suit: str, number: int): 

35 """ 

36 Initialize a card. 

37 

38 Args: 

39 color: The card color ("red" or "black") 

40 suit: The card suit ("heart", "diamond", "spade", or "club") 

41 number: The card number (1-13, where Ace=1, Jack=11, Queen=12, King=13) 

42 """ 

43 super().__init__(color=color, suit=suit, number=number) 

44 

45 def __repr__(self) -> str: 

46 return ( 

47 f"Card(color={self.properties['color']}, " 

48 f"suit={self.properties['suit']}, " 

49 f"number={self.properties['number']})" 

50 ) 

51 

52 

53def is_odd(n: int) -> bool: 

54 """Return True if n is odd.""" 

55 return n % 2 == 1 

56 

57 

58def is_even(n: int) -> bool: 

59 """Return True if n is even.""" 

60 return n % 2 == 0 

61 

62 

63def alternation_rule(seq: Sequence[AbstractObject]) -> bool: 

64 """If the last card was red, play a black card. If black, play red.""" 

65 if len(seq) < 2: 

66 return True # no prior card or only starter exists 

67 last = seq[-2] # last accepted card 

68 candidate = seq[-1] # newly played card 

69 if last["color"] == "red": 

70 return candidate["color"] == "black" 

71 elif last["color"] == "black": 

72 return candidate["color"] == "red" 

73 return True 

74 

75 

76# The suit cycle: spade->heart->diamond->club->spade 

77SUIT_CYCLE = {"spade": "heart", "heart": "diamond", "diamond": "club", "club": "spade"} 

78 

79 

80def suit_cycle_rule(seq: Sequence[AbstractObject]) -> bool: 

81 """Follow the suit cycle: spade->heart->diamond->club->spade.""" 

82 if len(seq) < 2: 

83 return True 

84 last = seq[-2] 

85 candidate = seq[-1] 

86 return SUIT_CYCLE[last["suit"]] == candidate["suit"] 

87 

88 

89def fixed_pattern_rule(seq: Sequence[AbstractObject]) -> bool: 

90 """Groups of 3 cards alternate between red and black.""" 

91 if len(seq) < 1: 

92 return True 

93 group_size = 3 

94 # Get the color of the first card in the sequence to determine pattern 

95 first_color = seq[0]["color"] 

96 # Calculate which group the current card is in 

97 group_num = (len(seq) - 1) // group_size 

98 # Expected color alternates based on first card's color 

99 expected_color = ( 

100 first_color 

101 if group_num % 2 == 0 

102 else ("black" if first_color == "red" else "red") 

103 ) 

104 return seq[-1]["color"] == expected_color 

105 

106 

107def odd_even_rule(seq: Sequence[AbstractObject]) -> bool: 

108 """If last number was odd, next must be even; if even, next must be odd.""" 

109 if len(seq) < 2: 

110 return True 

111 last = seq[-2] 

112 candidate = seq[-1] 

113 if is_odd(last["number"]): 

114 return is_even(candidate["number"]) 

115 elif is_even(last["number"]): 

116 return is_odd(candidate["number"]) 

117 return True 

118 

119 

120def range_rule(seq: Sequence[AbstractObject]) -> bool: 

121 """If last number was 1-7, next must be 8-13; if 8-13, next must be 1-7.""" 

122 if len(seq) < 2: 

123 return True 

124 last = seq[-2] 

125 candidate = seq[-1] 

126 if 1 <= last["number"] <= 7: 

127 return 8 <= candidate["number"] <= 13 

128 elif 8 <= last["number"] <= 13: 

129 return 1 <= candidate["number"] <= 7 

130 return True 

131 

132 

133def increment_rule(seq: Sequence[AbstractObject]) -> bool: 

134 """Next card's number must be 1-3 higher (modulo 13) than the last card.""" 

135 if len(seq) < 2: 

136 return True 

137 last = seq[-2] 

138 candidate = seq[-1] 

139 diff = (candidate["number"] - last["number"]) % 13 

140 return 1 <= diff <= 3 

141 

142 

143def hard_odd_even_color_rule(seq: Sequence[AbstractObject]) -> bool: 

144 """If current number is odd, it must be red; if even, it must be black.""" 

145 if len(seq) < 1: 

146 return True 

147 candidate = seq[-1] 

148 if is_odd(candidate["number"]): 

149 return candidate["color"] == "red" 

150 elif is_even(candidate["number"]): 

151 return candidate["color"] == "black" 

152 return True 

153 

154 

155def matching_rule(seq: Sequence[AbstractObject]) -> bool: 

156 """Next card must match previous card's suit or number.""" 

157 if len(seq) < 2: 

158 return True 

159 last = seq[-2] 

160 candidate = seq[-1] 

161 return candidate["suit"] == last["suit"] or candidate["number"] == last["number"] 

162 

163 

164def comparative_rule(seq: Sequence[AbstractObject]) -> bool: 

165 """If current card is black, its number must be <= previous; if red, its number must be >= previous.""" 

166 if len(seq) < 2: 

167 return True 

168 for i in range(1, len(seq)): 

169 current = seq[i] 

170 prev = seq[i - 1] 

171 # For black cards, current number must be less than or equal to previous 

172 if current["color"] == "black": 

173 if current["number"] > prev["number"]: 

174 return False 

175 # For red cards, current number must be greater than or equal to previous 

176 elif current["color"] == "red": 

177 if current["number"] < prev["number"]: 

178 return False 

179 return True 

180 

181 

182def fibonacci_rule(seq: Sequence[AbstractObject]) -> bool: 

183 """Numbers must follow Fibonacci sequence (mod 13).""" 

184 if len(seq) < 3: 

185 return True 

186 last_three = seq[-3:] 

187 nums = [n["number"] for n in last_three] 

188 return (nums[2] % 13) == ((nums[0] + nums[1]) % 13) 

189 

190 

191def prime_sum_rule(seq: Sequence[AbstractObject]) -> bool: 

192 """Sum of last three numbers must be prime.""" 

193 

194 def is_prime(n: int) -> bool: 

195 if n < 2: 

196 return False 

197 return all(n % i != 0 for i in range(2, int(math.sqrt(n)) + 1)) 

198 

199 if len(seq) < 3: 

200 return True 

201 last_three = seq[-3:] 

202 total = sum(card["number"] for card in last_three) 

203 return is_prime(total) 

204 

205 

206def royal_sequence_rule(seq: Sequence[AbstractObject]) -> bool: 

207 """ 

208 Face cards must appear in order: Jack -> Queen -> King, 

209 with any number of non-face cards between them. 

210 """ 

211 if len(seq) < 2: 

212 return True 

213 

214 # Track the last face card seen 

215 last_face = None 

216 for card in seq: 

217 num = card["number"] 

218 if num in {11, 12, 13}: # Face cards 

219 if last_face is None and num != 11: # Must start with Jack 

220 return False 

221 if last_face == 11 and num != 12: # Jack must be followed by Queen 

222 return False 

223 if last_face == 12 and num != 13: # Queen must be followed by King 

224 return False 

225 last_face = num 

226 return True 

227 

228 

229def create_suit_value_rule(suit_values: Dict[str, int]) -> DSLRule: 

230 """ 

231 Creates a rule where each suit has a point value, and consecutive 

232 cards must increase the total score. 

233 

234 Example: 

235 hearts_high = create_suit_value_rule({ 

236 "heart": 4, "diamond": 3, "club": 2, "spade": 1 

237 }) 

238 """ 

239 

240 def check_suit_values(seq: Sequence[AbstractObject]) -> bool: 

241 if len(seq) < 2: 

242 return True 

243 

244 def card_value(card: AbstractObject) -> int: 

245 return suit_values[card["suit"]] * card["number"] 

246 

247 last_value = card_value(seq[-2]) 

248 current_value = card_value(seq[-1]) 

249 return current_value > last_value 

250 

251 return DSLRule(check_suit_values, "Card values must increase") 

252 

253 

254def create_historical_rule(window: int = 3) -> DSLRule: 

255 """ 

256 Creates a rule requiring new cards to match a property 

257 (color, suit, or number) from the historical window. 

258 

259 Example: 

260 historical = create_historical_rule(3) # Must match last 3 cards 

261 """ 

262 

263 def check_historical(seq: Sequence[AbstractObject]) -> bool: 

264 if len(seq) <= window: 

265 return True 

266 

267 current = seq[-1] 

268 history = seq[-window - 1 : -1] 

269 

270 # Must match at least one property from history 

271 return any( 

272 current["color"] == card["color"] 

273 or current["suit"] == card["suit"] 

274 or current["number"] == card["number"] 

275 for card in history 

276 ) 

277 

278 return DSLRule(check_historical, f"Must match a property from last {window} cards") 

279 

280 

281def create_meta_rule(rules: List[DSLRule], required_count: int) -> DSLRule: 

282 """ 

283 Creates a meta-rule requiring a certain number of other rules to be satisfied. 

284 

285 Example: 

286 two_of_three = create_meta_rule([rule1, rule2, rule3], 2) 

287 """ 

288 

289 def check_meta(seq: Sequence[AbstractObject]) -> bool: 

290 satisfied = sum(1 for rule in rules if rule(seq)) 

291 return satisfied >= required_count 

292 

293 return DSLRule( 

294 check_meta, f"Must satisfy at least {required_count} of {len(rules)} rules" 

295 ) 

296 

297 

298def create_symmetry_rule(length: int = 3) -> DSLRule: 

299 """ 

300 Creates a rule requiring symmetry in card properties over a window. 

301 

302 Example: 

303 symmetry = create_symmetry_rule(3) # A-B-A pattern 

304 """ 

305 

306 def check_symmetry(seq: Sequence[AbstractObject]) -> bool: 

307 if len(seq) < length: 

308 return True 

309 

310 window = seq[-length:] 

311 for i in range(length // 2): 

312 left = window[i] 

313 right = window[-(i + 1)] 

314 if not ( 

315 left["color"] == right["color"] 

316 or left["suit"] == right["suit"] 

317 or left["number"] == right["number"] 

318 ): 

319 return False 

320 return True 

321 

322 return DSLRule(check_symmetry, f"Symmetric pattern over {length} cards") 

323 

324 

325def create_property_cycle_rule(*properties: str) -> DSLRule: 

326 """Create a rule that requires at least one consecutive pair of cards to match on each property in the cycle. 

327 

328 Args: 

329 *properties: Variable number of property names to check for matches. 

330 

331 Returns: 

332 A rule that returns True if at least one consecutive pair matches on each property in the cycle. 

333 """ 

334 

335 def property_cycle_rule(seq: List[Card]) -> bool: 

336 if len(seq) < 2: 

337 return True 

338 

339 # Track which properties have been matched 

340 matched_properties = set() 

341 

342 # Check each consecutive pair 

343 for i in range(len(seq) - 1): 

344 # Get current and next card 

345 curr_card = seq[i] 

346 next_card = seq[i + 1] 

347 

348 # Check which properties match for this pair 

349 for prop in properties: 

350 if curr_card[prop] == next_card[prop]: 

351 print( 

352 f"Pair {i} matches on {prop}: {curr_card[prop]} == {next_card[prop]}" 

353 ) 

354 matched_properties.add(prop) 

355 else: 

356 print( 

357 f"Pair {i} does not match on {prop}: {curr_card[prop]} != {next_card[prop]}" 

358 ) 

359 

360 # Check if we found a match for each property 

361 return len(matched_properties) == len(properties) 

362 

363 return DSLRule( 

364 property_cycle_rule, 

365 f"Each consecutive pair matches on cycling properties: {', '.join(properties)}", 

366 ) 

367 

368 

369# Create DSL rules for each function 

370alternation_dsl = DSLRule( 

371 alternation_rule, 

372 "Alternation: if last card is red, next is black; if black, next is red", 

373) 

374 

375suit_cycle_dsl = DSLRule( 

376 suit_cycle_rule, "Suit Cycle: follow spade->heart->diamond->club->spade" 

377) 

378 

379fixed_pattern_dsl = DSLRule( 

380 fixed_pattern_rule, "Fixed Pattern: groups of 3 cards alternate red and black" 

381) 

382 

383odd_even_dsl = DSLRule( 

384 odd_even_rule, "Odd/Even: if last number is odd, next is even; if even, next is odd" 

385) 

386 

387range_dsl = DSLRule( 

388 range_rule, "Range Rule: if last number is 1-7, next is 8-13; else vice versa" 

389) 

390 

391increment_dsl = DSLRule( 

392 increment_rule, "Increment: next card's number is 1-3 higher modulo 13" 

393) 

394 

395hard_odd_even_color_dsl = DSLRule( 

396 hard_odd_even_color_rule, 

397 "Hard Odd/Even Color: if current number is odd, it must be red; if even, it must be black", 

398) 

399 

400matching_dsl = DSLRule( 

401 matching_rule, "Matching: next card matches last card in suit or number" 

402) 

403 

404comparative_dsl = DSLRule( 

405 comparative_rule, 

406 "Comparative: if current card is black, its number must be <= previous; if red, its number must be >= previous", 

407) 

408 

409fibonacci_dsl = DSLRule(fibonacci_rule, "Numbers follow Fibonacci sequence modulo 13") 

410 

411prime_sum_dsl = DSLRule(prime_sum_rule, "Sum of last three numbers is prime") 

412 

413royal_sequence_dsl = DSLRule(royal_sequence_rule, "Face cards appear in order: J->Q->K") 

414 

415# Create example rules 

416hearts_high = create_suit_value_rule({"heart": 4, "diamond": 3, "club": 2, "spade": 1}) 

417 

418historical_pattern = create_historical_rule(3) 

419 

420symmetry_rule = create_symmetry_rule(3) 

421 

422property_cycle = create_property_cycle_rule("color", "number") 

423 

424# Complex rule combinations 

425mathematical_rules = create_meta_rule( 

426 [fibonacci_dsl, prime_sum_dsl, range_dsl], required_count=2 

427) 

428 

429royal_rules = create_meta_rule( 

430 [royal_sequence_dsl, matching_dsl, comparative_dsl], required_count=2 

431) 

432 

433pattern_rules = create_meta_rule( 

434 [symmetry_rule, property_cycle, historical_pattern], required_count=2 

435) 

436 

437# Collection of all rules, organized by category 

438eleusis_rules: Dict[str, DSLRule] = { 

439 # Classic Rules 

440 "alternation": alternation_dsl, 

441 "suit_cycle": suit_cycle_dsl, 

442 "fixed_pattern": fixed_pattern_dsl, 

443 "odd_even": odd_even_dsl, 

444 "range": range_dsl, 

445 "increment": increment_dsl, 

446 "hard_odd_even_color": hard_odd_even_color_dsl, 

447 "matching": matching_dsl, 

448 "comparative": comparative_dsl, 

449 # Mathematical Rules 

450 "fibonacci": fibonacci_dsl, 

451 "prime_sum": prime_sum_dsl, 

452 # Card Relationship Rules 

453 "royal_sequence": royal_sequence_dsl, 

454 "hearts_high": hearts_high, 

455 "historical_pattern": historical_pattern, 

456 "symmetry": symmetry_rule, 

457 # Property Rules 

458 "property_cycle": property_cycle, 

459 # Meta Rules 

460 "mathematical_combo": mathematical_rules, 

461 "royal_combo": royal_rules, 

462 "pattern_combo": pattern_rules, 

463} 

464 

465# Rule categories for easier access 

466classic_rules = { 

467 key: eleusis_rules[key] 

468 for key in [ 

469 "alternation", 

470 "suit_cycle", 

471 "fixed_pattern", 

472 "odd_even", 

473 "range", 

474 "increment", 

475 "hard_odd_even_color", 

476 "matching", 

477 "comparative", 

478 ] 

479} 

480 

481mathematical_rules = {key: eleusis_rules[key] for key in ["fibonacci", "prime_sum"]} 

482 

483relationship_rules = { 

484 key: eleusis_rules[key] 

485 for key in ["royal_sequence", "hearts_high", "historical_pattern", "symmetry"] 

486} 

487 

488property_rules = {key: eleusis_rules[key] for key in ["property_cycle"]} 

489 

490meta_rules = { 

491 key: eleusis_rules[key] 

492 for key in ["mathematical_combo", "royal_combo", "pattern_combo"] 

493} 

494 

495# Example rule combinations for different difficulty levels 

496beginner_rules = [ 

497 eleusis_rules["alternation"], 

498 eleusis_rules["odd_even"], 

499 eleusis_rules["matching"], 

500] 

501 

502intermediate_rules = [ 

503 eleusis_rules["suit_cycle"], 

504 eleusis_rules["royal_sequence"], 

505 eleusis_rules["historical_pattern"], 

506] 

507 

508advanced_rules = [ 

509 eleusis_rules["fibonacci"], 

510 eleusis_rules["symmetry"], 

511 eleusis_rules["pattern_combo"], 

512] 

513 

514# Create meta-rules for different difficulty levels 

515beginner_meta = create_meta_rule(beginner_rules, 2) 

516intermediate_meta = create_meta_rule(intermediate_rules, 2) 

517advanced_meta = create_meta_rule(advanced_rules, 2) 

518 

519# Example sequences demonstrating creative rules 

520fibonacci_sequence = [ 

521 Card(color="red", suit="heart", number=1), 

522 Card(color="black", suit="spade", number=1), 

523 Card(color="red", suit="diamond", number=2), 

524 Card(color="black", suit="club", number=3), 

525 Card(color="red", suit="heart", number=5), 

526] 

527 

528royal_pattern = [ 

529 Card(color="red", suit="heart", number=11), # Jack 

530 Card(color="black", suit="spade", number=5), # Non-face 

531 Card(color="red", suit="diamond", number=12), # Queen 

532 Card(color="black", suit="club", number=7), # Non-face 

533 Card(color="red", suit="heart", number=13), # King 

534] 

535 

536symmetric_pattern = [ 

537 Card(color="red", suit="heart", number=7), 

538 Card(color="black", suit="spade", number=10), 

539 Card(color="red", suit="diamond", number=7), 

540]