Skip to content

Commit 230696b

Browse files
thompsonsonclaude
andcommitted
feat: add multi-evidence extension with configurable evidence types
- Add Basic and Extended evidence type configuration - Implement multi-evidence generation (e.g., ["lower", "half"]) - Update Bayesian inference for joint probability calculations - Add UI dropdown for evidence type selection - Maintain domain separation and architectural constraints - Update all tests (83 passing) for new multi-evidence format Basic Evidence: higher/lower/same Extended Evidence: adds half/double for richer inference 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 0f5f162 commit 230696b

File tree

9 files changed

+349
-123
lines changed

9 files changed

+349
-123
lines changed

CLAUDE.md

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,39 @@ A Bayesian Game implementation featuring a Belief-based Agent using domain-drive
66
## Game Rules
77
- Judge and Player 1 can see the target die value
88
- Player 2 must deduce the target value using only comparison results
9-
- Player 1 rolls dice and reports "higher"/"lower"/"same" compared to target
10-
- **CRITICAL**: Player 2 receives ONLY the comparison result, NOT the dice roll value
9+
- Player 1 rolls dice and reports evidence based on selected evidence type
10+
- **CRITICAL**: Player 2 receives ONLY the evidence results, NOT the dice roll value
1111
- Game runs for 10 rounds
1212
- Judge ensures truth-telling
1313

14+
### Evidence Types
15+
**Basic Evidence**: `["higher", "lower", "same"]`
16+
- Standard comparison between dice roll and target
17+
18+
**Extended Evidence**: `["higher", "lower", "same", "half", "double"]`
19+
- Multiple evidence types can apply to single roll
20+
- "half": dice_roll = target/2 (exact integer matches only)
21+
- "double": dice_roll = target*2 (exact integer matches only)
22+
- Example: target=4, dice_roll=2 → evidence=`["lower", "half"]`
23+
1424
## Development Practices
1525
- Use conventional commits when committing code to git
1626
- Always use uv and the local venv
27+
- Always use the make file for devops-style tasks
1728

1829
## Architecture
1930
Domain-Driven Design with 3 modules:
2031

2132
1. **Environment Domain** (`domains/environment/environment_domain.py`)
22-
- EnvironmentEvidence dataclass (contains dice_roll AND comparison_result)
23-
- Environment class for target/evidence generation
33+
- EnvironmentEvidence dataclass (contains dice_roll AND comparison_results)
34+
- Environment class for target/evidence generation with configurable evidence types
2435
- **ACCESS**: Full knowledge of dice rolls and target values
2536

2637
2. **Belief Domain** (`domains/belief/belief_domain.py`)
27-
- BeliefUpdate dataclass (contains ONLY comparison_result)
28-
- BayesianBeliefState class for inference
38+
- BeliefUpdate dataclass (contains ONLY comparison_results as List[str])
39+
- BayesianBeliefState class for inference with multi-evidence support
2940
- **ACCESS**: NO knowledge of dice roll values or true target
30-
- **CONSTRAINT**: Must calculate P(comparison_result | target) probabilistically
41+
- **CONSTRAINT**: Must calculate P(comparison_results | target) probabilistically for multiple evidence types
3142

3243
3. **Game Coordination** (`domains/coordination/game_coordination.py`)
3344
- GameState dataclass (tracks full game state)
@@ -63,43 +74,55 @@ bayesian_game/
6374
## Key Design Decisions & Architectural Constraints
6475

6576
### Information Flow Rules
66-
1. **Environment → Coordination**: EnvironmentEvidence (dice_roll + comparison_result)
67-
2. **Coordination → Belief**: BeliefUpdate (comparison_result ONLY)
77+
1. **Environment → Coordination**: EnvironmentEvidence (dice_roll + comparison_results)
78+
2. **Coordination → Belief**: BeliefUpdate (comparison_results ONLY as List[str])
6879
3. **NEVER**: Direct Environment → Belief communication
6980
4. **NEVER**: Belief domain access to dice roll values
7081

82+
### Multi-Evidence Processing
83+
- Environment generates all applicable evidence types for each roll
84+
- Coordination filters dice_roll information before passing to belief domain
85+
- Belief domain calculates joint probabilities: P(comparison_results | target)
86+
- UI displays evidence configuration options (Basic vs Extended)
87+
7188
### Domain Separation Principles
7289
- **Environment Domain**: No probability knowledge, pure evidence generation
7390
- **Belief Domain**: Pure Bayesian inference, no knowledge of actual dice values
7491
- **Coordination Layer**: Thin orchestration, responsible for information filtering
7592
- **UI Layer**: Separate from core game logic, can display full information
7693

7794
### Critical Implementation Rules
78-
- BeliefUpdate dataclass MUST contain only comparison_result
79-
- BayesianBeliefState MUST calculate P(comparison_result | target) probabilistically
95+
- BeliefUpdate dataclass MUST contain only comparison_results as List[str]
96+
- BayesianBeliefState MUST calculate P(comparison_results | target) probabilistically for multi-evidence
8097
- Game coordination MUST filter dice_roll from EnvironmentEvidence before passing to belief domain
8198
- Tests MUST verify that belief domain never receives dice roll values
99+
- Evidence type configuration MUST be passed through coordination layer, not directly to belief domain
82100

83101
## Maintaining Architectural Integrity
84102

85103
### Code Review Checklist
86104
When modifying the codebase, ensure:
87-
- [ ] BeliefUpdate contains ONLY comparison_result field
105+
- [ ] BeliefUpdate contains ONLY comparison_results field (List[str])
88106
- [ ] No dice_roll parameter passed to belief domain methods
89107
- [ ] Game coordination filters EnvironmentEvidence properly
90108
- [ ] Tests verify belief domain isolation
91-
- [ ] Belief calculations use probabilistic formulas, not direct dice values
109+
- [ ] Belief calculations use probabilistic formulas for multi-evidence: P(comparison_results | target)
110+
- [ ] Evidence type configuration flows through coordination layer
111+
- [ ] UI evidence type selection properly configures game behavior
92112

93113
### Anti-Patterns to Avoid
94-
`BeliefUpdate(dice_roll=X, comparison_result=Y)` - belief shouldn't know dice value
114+
`BeliefUpdate(dice_roll=X, comparison_results=Y)` - belief shouldn't know dice value
95115
❌ Direct Environment-Belief communication
96116
❌ Belief domain knowing actual dice roll or target values
97-
❌ Hard-coded probability values instead of calculated P(comparison_result | target)
117+
❌ Hard-coded probability values instead of calculated P(comparison_results | target)
118+
❌ Passing evidence type configuration directly to belief domain
98119

99120
### Correct Patterns
100-
`BeliefUpdate(comparison_result="higher")` - only comparison result
121+
`BeliefUpdate(comparison_results=["lower", "half"])` - only evidence results
101122
✅ Environment → Coordination → Belief information flow
102-
✅ Probabilistic calculations: P(roll > target) = (dice_sides - target) / dice_sides
123+
✅ Probabilistic calculations for multi-evidence: P(comparison_results | target)
124+
✅ Evidence type configuration handled in coordination layer
125+
✅ Joint probability calculations: P(["lower", "half"] | target) = P(dice_roll=target/2 AND dice_roll<target)
103126
✅ Clean domain boundaries with no cross-dependencies
104127

105128
## Dependencies

domains/belief/belief_domain.py

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from dataclasses import dataclass
2-
from typing import Literal
32

43
import numpy as np
54

@@ -8,7 +7,7 @@
87
class BeliefUpdate:
98
"""Update information for Bayesian belief state."""
109

11-
comparison_result: Literal["higher", "lower", "same"]
10+
comparison_results: list[str]
1211

1312

1413
class BayesianBeliefState:
@@ -65,26 +64,19 @@ def update_beliefs(self, evidence: BeliefUpdate) -> None:
6564
"""
6665
self.evidence_history.append(evidence)
6766

68-
comparison_result = evidence.comparison_result
67+
comparison_results = evidence.comparison_results
6968

7069
# Calculate likelihood for each possible target value
7170
likelihoods = np.zeros(self.dice_sides)
7271

7372
for target_idx in range(self.dice_sides):
7473
target_value = target_idx + 1
7574

76-
# Calculate P(comparison_result | target_value)
77-
# This is the probability that ANY dice roll would produce this comparison result
78-
if comparison_result == "higher":
79-
# P(roll > target) = (dice_sides - target) / dice_sides
80-
likelihood = (self.dice_sides - target_value) / self.dice_sides
81-
elif comparison_result == "lower":
82-
# P(roll < target) = (target - 1) / dice_sides
83-
likelihood = (target_value - 1) / self.dice_sides
84-
else: # comparison_result == "same"
85-
# P(roll = target) = 1 / dice_sides
86-
likelihood = 1 / self.dice_sides
87-
75+
# Calculate P(comparison_results | target_value)
76+
# This is the joint probability that a dice roll would produce ALL these evidence types
77+
likelihood = self._calculate_joint_likelihood(
78+
comparison_results, target_value
79+
)
8880
likelihoods[target_idx] = likelihood
8981

9082
# Apply Bayes' rule: posterior ∝ prior * likelihood
@@ -99,6 +91,54 @@ def update_beliefs(self, evidence: BeliefUpdate) -> None:
9991
# reset to uniform distribution
10092
self.beliefs = np.ones(self.dice_sides) / self.dice_sides
10193

94+
def _calculate_joint_likelihood(
95+
self, comparison_results: list[str], target_value: int
96+
) -> float:
97+
"""Calculate P(comparison_results | target_value) for multiple evidence types.
98+
99+
Args:
100+
comparison_results: List of evidence results (e.g., ["lower", "half"])
101+
target_value: Target value to calculate likelihood for
102+
103+
Returns:
104+
Joint probability of observing all evidence types given the target
105+
"""
106+
# For multiple evidence types from a single roll, we need to find
107+
# the probability that a single dice roll satisfies ALL conditions
108+
109+
# Count dice rolls that satisfy all evidence conditions
110+
satisfying_rolls = 0
111+
112+
for dice_roll in range(1, self.dice_sides + 1):
113+
satisfies_all = True
114+
115+
for evidence in comparison_results:
116+
if (
117+
(evidence == "higher" and not (dice_roll > target_value))
118+
or (evidence == "lower" and not (dice_roll < target_value))
119+
or (evidence == "same" and dice_roll != target_value)
120+
or (
121+
evidence == "half"
122+
and not (
123+
target_value % 2 == 0 and dice_roll == target_value // 2
124+
)
125+
)
126+
or (
127+
evidence == "double"
128+
and not (
129+
dice_roll == target_value * 2
130+
and dice_roll <= self.dice_sides
131+
)
132+
)
133+
):
134+
satisfies_all = False
135+
break
136+
137+
if satisfies_all:
138+
satisfying_rolls += 1
139+
140+
return satisfying_rolls / self.dice_sides
141+
102142
def reset_beliefs(self) -> None:
103143
"""Reset beliefs to uniform prior and clear evidence history."""
104144
self.beliefs = np.ones(self.dice_sides) / self.dice_sides

domains/coordination/game_coordination.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
from typing import Any
44

55
from ..belief.belief_domain import BayesianBeliefState, BeliefUpdate
6-
from ..environment.environment_domain import Environment, EnvironmentEvidence
6+
from ..environment.environment_domain import (
7+
Environment,
8+
EnvironmentEvidence,
9+
EvidenceType,
10+
)
711

812

913
class GamePhase(Enum):
@@ -42,20 +46,28 @@ class BayesianGame:
4246
"""
4347

4448
def __init__(
45-
self, dice_sides: int = 6, max_rounds: int = 10, seed: int | None = None
49+
self,
50+
dice_sides: int = 6,
51+
max_rounds: int = 10,
52+
evidence_type: EvidenceType = EvidenceType.BASIC,
53+
seed: int | None = None,
4654
):
4755
"""Initialize the Bayesian Game.
4856
4957
Args:
5058
dice_sides: Number of sides on the dice
5159
max_rounds: Maximum number of rounds to play
60+
evidence_type: Type of evidence to generate (basic or extended)
5261
seed: Random seed for reproducible results
5362
"""
5463
self.dice_sides = dice_sides
5564
self.max_rounds = max_rounds
65+
self.evidence_type = evidence_type
5666

5767
# Initialize domains
58-
self.environment = Environment(dice_sides=dice_sides, seed=seed)
68+
self.environment = Environment(
69+
dice_sides=dice_sides, evidence_type=evidence_type, seed=seed
70+
)
5971
self.belief_state = BayesianBeliefState(dice_sides=dice_sides)
6072

6173
# Initialize game state
@@ -113,8 +125,8 @@ def play_round(self) -> GameState:
113125
# Generate evidence from environment
114126
evidence = self.environment.roll_dice_and_compare()
115127

116-
# Update belief state (only pass comparison result, not dice roll)
117-
belief_update = BeliefUpdate(comparison_result=evidence.comparison_result)
128+
# Update belief state (only pass comparison results, not dice roll)
129+
belief_update = BeliefUpdate(comparison_results=evidence.comparison_results)
118130
self.belief_state.update_beliefs(belief_update)
119131

120132
# Update game state

domains/environment/environment_domain.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import random
22
from dataclasses import dataclass
3-
from typing import Literal
3+
from enum import Enum
4+
5+
6+
class EvidenceType(Enum):
7+
"""Types of evidence that can be generated."""
8+
9+
BASIC = "basic"
10+
EXTENDED = "extended"
411

512

613
@dataclass
714
class EnvironmentEvidence:
8-
"""Evidence generated by the environment - dice roll and comparison result."""
15+
"""Evidence generated by the environment - dice roll and comparison results."""
916

1017
dice_roll: int
11-
comparison_result: Literal["higher", "lower", "same"]
18+
comparison_results: list[str]
1219

1320

1421
class Environment:
@@ -17,14 +24,21 @@ class Environment:
1724
Has no knowledge of probabilities - purely generates observable evidence.
1825
"""
1926

20-
def __init__(self, dice_sides: int = 6, seed: int | None = None):
27+
def __init__(
28+
self,
29+
dice_sides: int = 6,
30+
evidence_type: EvidenceType = EvidenceType.BASIC,
31+
seed: int | None = None,
32+
):
2133
"""Initialize environment with dice configuration.
2234
2335
Args:
2436
dice_sides: Number of sides on the dice (default 6)
37+
evidence_type: Type of evidence to generate (basic or extended)
2538
seed: Random seed for reproducible results
2639
"""
2740
self.dice_sides = dice_sides
41+
self.evidence_type = evidence_type
2842
self._random_state = (
2943
random.Random(seed) if seed is not None else random.Random()
3044
)
@@ -67,7 +81,7 @@ def roll_dice_and_compare(self) -> EnvironmentEvidence:
6781
"""Roll dice and compare to target, generating evidence.
6882
6983
Returns:
70-
EnvironmentEvidence with dice roll and comparison result
84+
EnvironmentEvidence with dice roll and comparison results
7185
7286
Raises:
7387
ValueError: If target value hasn't been set
@@ -76,14 +90,26 @@ def roll_dice_and_compare(self) -> EnvironmentEvidence:
7690
raise ValueError("Target value not set")
7791

7892
dice_roll = self._random_state.randint(1, self.dice_sides)
93+
comparison_results = []
7994

95+
# Basic evidence: higher/lower/same
8096
if dice_roll > self._target_value:
81-
comparison_result = "higher"
97+
comparison_results.append("higher")
8298
elif dice_roll < self._target_value:
83-
comparison_result = "lower"
99+
comparison_results.append("lower")
84100
else:
85-
comparison_result = "same"
101+
comparison_results.append("same")
102+
103+
# Extended evidence: half/double (only for extended type)
104+
if self.evidence_type == EvidenceType.EXTENDED:
105+
# Check for "half" - dice_roll = target/2 (exact integer only)
106+
if self._target_value % 2 == 0 and dice_roll == self._target_value // 2:
107+
comparison_results.append("half")
108+
109+
# Check for "double" - dice_roll = target*2 (within dice range)
110+
if dice_roll == self._target_value * 2 and dice_roll <= self.dice_sides:
111+
comparison_results.append("double")
86112

87113
return EnvironmentEvidence(
88-
dice_roll=dice_roll, comparison_result=comparison_result
114+
dice_roll=dice_roll, comparison_results=comparison_results
89115
)

0 commit comments

Comments
 (0)