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
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-27 10:39 -0600
1"""
2Classic Eleusis card game rules.
4This module implements both classic and creative rules for the Eleusis card game,
5showcasing a wide range of pattern matching capabilities:
7Classic Rules:
8- Red/black alternation
9- Suit cycles
10- Fixed patterns
11- Odd/even numbers
12- Range-based rules
13- Increment patterns
15Creative Rules:
16- Mathematical patterns
17- Multi-card relationships
18- Historical patterns
19- Card properties combinations
20- Complex sequences
21- Meta-rules
22"""
24import math
25from typing import Dict, List, Sequence
27from ..core import AbstractObject
28from ..dsl import DSLRule
31class Card(AbstractObject):
32 """A playing card with color, suit, and number properties."""
34 def __init__(self, color: str, suit: str, number: int):
35 """
36 Initialize a card.
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)
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 )
53def is_odd(n: int) -> bool:
54 """Return True if n is odd."""
55 return n % 2 == 1
58def is_even(n: int) -> bool:
59 """Return True if n is even."""
60 return n % 2 == 0
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
76# The suit cycle: spade->heart->diamond->club->spade
77SUIT_CYCLE = {"spade": "heart", "heart": "diamond", "diamond": "club", "club": "spade"}
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"]
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
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
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
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
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
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"]
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
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)
191def prime_sum_rule(seq: Sequence[AbstractObject]) -> bool:
192 """Sum of last three numbers must be prime."""
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))
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)
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
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
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.
234 Example:
235 hearts_high = create_suit_value_rule({
236 "heart": 4, "diamond": 3, "club": 2, "spade": 1
237 })
238 """
240 def check_suit_values(seq: Sequence[AbstractObject]) -> bool:
241 if len(seq) < 2:
242 return True
244 def card_value(card: AbstractObject) -> int:
245 return suit_values[card["suit"]] * card["number"]
247 last_value = card_value(seq[-2])
248 current_value = card_value(seq[-1])
249 return current_value > last_value
251 return DSLRule(check_suit_values, "Card values must increase")
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.
259 Example:
260 historical = create_historical_rule(3) # Must match last 3 cards
261 """
263 def check_historical(seq: Sequence[AbstractObject]) -> bool:
264 if len(seq) <= window:
265 return True
267 current = seq[-1]
268 history = seq[-window - 1 : -1]
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 )
278 return DSLRule(check_historical, f"Must match a property from last {window} cards")
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.
285 Example:
286 two_of_three = create_meta_rule([rule1, rule2, rule3], 2)
287 """
289 def check_meta(seq: Sequence[AbstractObject]) -> bool:
290 satisfied = sum(1 for rule in rules if rule(seq))
291 return satisfied >= required_count
293 return DSLRule(
294 check_meta, f"Must satisfy at least {required_count} of {len(rules)} rules"
295 )
298def create_symmetry_rule(length: int = 3) -> DSLRule:
299 """
300 Creates a rule requiring symmetry in card properties over a window.
302 Example:
303 symmetry = create_symmetry_rule(3) # A-B-A pattern
304 """
306 def check_symmetry(seq: Sequence[AbstractObject]) -> bool:
307 if len(seq) < length:
308 return True
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
322 return DSLRule(check_symmetry, f"Symmetric pattern over {length} cards")
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.
328 Args:
329 *properties: Variable number of property names to check for matches.
331 Returns:
332 A rule that returns True if at least one consecutive pair matches on each property in the cycle.
333 """
335 def property_cycle_rule(seq: List[Card]) -> bool:
336 if len(seq) < 2:
337 return True
339 # Track which properties have been matched
340 matched_properties = set()
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]
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 )
360 # Check if we found a match for each property
361 return len(matched_properties) == len(properties)
363 return DSLRule(
364 property_cycle_rule,
365 f"Each consecutive pair matches on cycling properties: {', '.join(properties)}",
366 )
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)
375suit_cycle_dsl = DSLRule(
376 suit_cycle_rule, "Suit Cycle: follow spade->heart->diamond->club->spade"
377)
379fixed_pattern_dsl = DSLRule(
380 fixed_pattern_rule, "Fixed Pattern: groups of 3 cards alternate red and black"
381)
383odd_even_dsl = DSLRule(
384 odd_even_rule, "Odd/Even: if last number is odd, next is even; if even, next is odd"
385)
387range_dsl = DSLRule(
388 range_rule, "Range Rule: if last number is 1-7, next is 8-13; else vice versa"
389)
391increment_dsl = DSLRule(
392 increment_rule, "Increment: next card's number is 1-3 higher modulo 13"
393)
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)
400matching_dsl = DSLRule(
401 matching_rule, "Matching: next card matches last card in suit or number"
402)
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)
409fibonacci_dsl = DSLRule(fibonacci_rule, "Numbers follow Fibonacci sequence modulo 13")
411prime_sum_dsl = DSLRule(prime_sum_rule, "Sum of last three numbers is prime")
413royal_sequence_dsl = DSLRule(royal_sequence_rule, "Face cards appear in order: J->Q->K")
415# Create example rules
416hearts_high = create_suit_value_rule({"heart": 4, "diamond": 3, "club": 2, "spade": 1})
418historical_pattern = create_historical_rule(3)
420symmetry_rule = create_symmetry_rule(3)
422property_cycle = create_property_cycle_rule("color", "number")
424# Complex rule combinations
425mathematical_rules = create_meta_rule(
426 [fibonacci_dsl, prime_sum_dsl, range_dsl], required_count=2
427)
429royal_rules = create_meta_rule(
430 [royal_sequence_dsl, matching_dsl, comparative_dsl], required_count=2
431)
433pattern_rules = create_meta_rule(
434 [symmetry_rule, property_cycle, historical_pattern], required_count=2
435)
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}
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}
481mathematical_rules = {key: eleusis_rules[key] for key in ["fibonacci", "prime_sum"]}
483relationship_rules = {
484 key: eleusis_rules[key]
485 for key in ["royal_sequence", "hearts_high", "historical_pattern", "symmetry"]
486}
488property_rules = {key: eleusis_rules[key] for key in ["property_cycle"]}
490meta_rules = {
491 key: eleusis_rules[key]
492 for key in ["mathematical_combo", "royal_combo", "pattern_combo"]
493}
495# Example rule combinations for different difficulty levels
496beginner_rules = [
497 eleusis_rules["alternation"],
498 eleusis_rules["odd_even"],
499 eleusis_rules["matching"],
500]
502intermediate_rules = [
503 eleusis_rules["suit_cycle"],
504 eleusis_rules["royal_sequence"],
505 eleusis_rules["historical_pattern"],
506]
508advanced_rules = [
509 eleusis_rules["fibonacci"],
510 eleusis_rules["symmetry"],
511 eleusis_rules["pattern_combo"],
512]
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)
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]
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]
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]