File size: 16,192 Bytes
6d5047c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
"""
Card 3: Shared State Loop (Deterministic Event Scheduler)

Defines deterministic event ordering for multi-character interactions in one port.
- Synchronized time with per-character state containers
- Deterministic conflict resolution (same seed β†’ same outcome)
- Support for β‰₯3 active characters
"""

from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Optional, Any
import hashlib
import json
import logging


LOGGER = logging.getLogger(__name__)


class CharacterState(Enum):
    """Character lifecycle state."""
    IDLE = "idle"
    BUSY = "busy"
    TRANSITIONING = "transitioning"
    INTERACTING = "interacting"


class ConflictResolutionPolicy(str, Enum):
    """How to resolve conflicting interactions."""
    PRIORITY_BASED = "priority_based"  # Higher priority character wins
    FIFO = "fifo"  # First in, first out
    COOLDOWN = "cooldown"  # Enforce cooldown between interactions
    NEGOTIATION = "negotiation"  # Custom negotiation logic


@dataclass
class CharacterSegmentState:
    """Current state of a character's motion segment execution."""
    
    character_id: str
    segment_index: int  # Current segment in script
    frames_elapsed: int  # Frames executed in current segment
    total_frames: int  # Total frames for current segment
    is_complete: bool = False
    
    def progress(self) -> float:
        """Return 0-1 progress through current segment."""
        if self.total_frames == 0:
            return 1.0
        return min(1.0, self.frames_elapsed / self.total_frames)


@dataclass
class CharacterSlot:
    """Per-character state container in shared loop."""
    
    character_id: str
    skeleton_type: str
    current_state: CharacterState = CharacterState.IDLE
    segment_state: Optional[CharacterSegmentState] = None
    
    # Interaction tracking
    interaction_target: Optional[str] = None
    last_interaction_time_ms: int = 0
    interaction_cooldown_ms: int = 500  # Prevent rapid re-interactions
    
    # Metadata
    priority: int = 0  # For conflict resolution
    cycle_count: int = 0  # Lifecycle counter
    
    def is_busy(self) -> bool:
        """Check if character is currently executing motion."""
        return self.current_state in [
            CharacterState.BUSY,
            CharacterState.TRANSITIONING,
            CharacterState.INTERACTING
        ]
    
    def can_interact(self, current_time_ms: int) -> bool:
        """Check if character can start new interaction."""
        time_since_last = current_time_ms - self.last_interaction_time_ms
        return time_since_last >= self.interaction_cooldown_ms


@dataclass
class LoopTick:
    """Single tick in the deterministic event loop."""
    
    tick_number: int
    frame_number: int
    time_ms: float
    fps: int = 30
    
    # Per-tick events
    character_updates: Dict[str, CharacterSlot] = field(default_factory=dict)
    completed_segments: List[str] = field(default_factory=list)
    interactions: List[tuple] = field(default_factory=list)  # [(from_id, to_id), ...]
    
    def get_timestamp(self) -> dict:
        """Return tick metadata for auditing."""
        return {
            "tick_number": self.tick_number,
            "frame_number": self.frame_number,
            "time_ms": self.time_ms,
            "fps": self.fps,
        }


class DeterministicLoop:
    """
    Deterministic multi-character event loop.
    
    Ensures:
    - Same seed β†’ same outputs (for testing replay)
    - No race conditions (total determinism within single process)
    - Clear conflict resolution (priority/FIFO/cooldown)
    - Synchronized timeline for all characters
    """
    
    def __init__(
        self,
        fps: int = 30,
        seed: int = 42,
        conflict_policy: ConflictResolutionPolicy = ConflictResolutionPolicy.COOLDOWN,
    ):
        LOGGER.info(
            "scheduler.loop.init.start fps=%s seed=%s conflict_policy=%s",
            fps,
            seed,
            conflict_policy.value,
        )
        self.fps = fps
        self.seed = seed
        self.conflict_policy = conflict_policy
        
        # Derive deterministic RNG state from seed
        self._rng_state = seed
        
        # State tracking
        self.tick_number = 0
        self.frame_number = 0
        self.time_ms = 0.0
        self.ms_per_frame = 1000.0 / fps
        
        # Per-character state
        self.characters: Dict[str, CharacterSlot] = {}
        
        # Event log for auditing
        self.tick_history: List[LoopTick] = []
        LOGGER.info("scheduler.loop.init.exit")
    
    def register_character(
        self,
        character_id: str,
        skeleton_type: str,
        priority: int = 0,
    ) -> None:
        """Register a character for this loop."""
        LOGGER.info(
            "scheduler.register_character.start character_id=%s skeleton=%s priority=%s",
            character_id,
            skeleton_type,
            priority,
        )
        if character_id in self.characters:
            raise ValueError(f"Character {character_id} already registered")
        
        self.characters[character_id] = CharacterSlot(
            character_id=character_id,
            skeleton_type=skeleton_type,
            priority=priority,
        )
        LOGGER.info("scheduler.register_character.exit character_id=%s", character_id)
    
    def _deterministic_rng(self) -> float:
        """Generate deterministic pseudo-random number (0-1)."""
        # Simple linear congruential generator seeded with loop state
        self._rng_state = (self._rng_state * 1103515245 + 12345) & 0x7fffffff
        return (self._rng_state / 0x7fffffff)
    
    def _resolve_conflict(
        self,
        char1_id: str,
        char2_id: str,
    ) -> str:
        """
        Deterministically resolve conflict between two characters.
        
        Returns: character_id that wins the interaction.
        """
        char1 = self.characters[char1_id]
        char2 = self.characters[char2_id]
        
        if self.conflict_policy == ConflictResolutionPolicy.PRIORITY_BASED:
            # Higher priority wins
            if char1.priority > char2.priority:
                return char1_id
            elif char2.priority > char1.priority:
                return char2_id
            # Equal priority: use deterministic tiebreaker (alphabetical)
            return min(char1_id, char2_id)
        
        elif self.conflict_policy == ConflictResolutionPolicy.FIFO:
            # Earlier interaction time wins
            if char1.last_interaction_time_ms < char2.last_interaction_time_ms:
                return char1_id
            else:
                return char2_id
        
        elif self.conflict_policy == ConflictResolutionPolicy.COOLDOWN:
            # Both can interact if cooldown satisfied
            char1_ready = char1.can_interact(int(self.time_ms))
            char2_ready = char2.can_interact(int(self.time_ms))
            
            if char1_ready and not char2_ready:
                return char1_id
            elif char2_ready and not char1_ready:
                return char2_id
            # Both or neither ready: use priority tiebreaker
            if char1.priority > char2.priority:
                return char1_id
            else:
                return char2_id
        
        else:  # NEGOTIATION (placeholder)
            return min(char1_id, char2_id)
    
    def advance_tick(
        self,
        character_motions: Dict[str, Dict[str, Any]],
    ) -> LoopTick:
        """
        Advance one tick forward with deterministic character updates.
        
        Args:
            character_motions: Dict[character_id] β†’ motion data for this frame
        
        Returns:
            LoopTick with event history for this frame
        """
        LOGGER.info(
            "scheduler.advance_tick.start tick=%s frame=%s chars=%s motions=%s",
            self.tick_number,
            self.frame_number,
            len(self.characters),
            len(character_motions),
        )
        tick = LoopTick(
            tick_number=self.tick_number,
            frame_number=self.frame_number,
            time_ms=self.time_ms,
            fps=self.fps,
        )
        
        # 1. Update character segment states (deterministic progression)
        for char_id, char_slot in self.characters.items():
            if char_slot.segment_state is None:
                continue
            
            # Advance frame counter
            char_slot.segment_state.frames_elapsed += 1
            
            # Check if segment complete
            if char_slot.segment_state.frames_elapsed >= char_slot.segment_state.total_frames:
                char_slot.segment_state.is_complete = True
                tick.completed_segments.append(char_id)
                char_slot.current_state = CharacterState.IDLE
            else:
                char_slot.current_state = CharacterState.BUSY
            
            tick.character_updates[char_id] = char_slot
        
        # 2. Detect and resolve conflicts
        pending_interactions = []
        for char_id, char_slot in self.characters.items():
            if char_slot.interaction_target:
                pending_interactions.append((char_id, char_slot.interaction_target))
        
        # Resolve conflicts deterministically
        for char1_id, char2_id in pending_interactions:
            winner_id = self._resolve_conflict(char1_id, char2_id)
            tick.interactions.append((winner_id, char2_id if winner_id == char1_id else char1_id))
            
            # Update last interaction time
            self.characters[winner_id].last_interaction_time_ms = int(self.time_ms)
        
        # 3. Advance time
        self.tick_number += 1
        self.frame_number += 1
        self.time_ms += self.ms_per_frame
        
        # 4. Record tick
        self.tick_history.append(tick)

        LOGGER.info(
            "scheduler.advance_tick.exit tick=%s completed=%s interactions=%s",
            tick.tick_number,
            len(tick.completed_segments),
            len(tick.interactions),
        )
        
        return tick
    
    def get_state_hash(self) -> str:
        """
        Compute deterministic hash of current loop state.
        
        Used for seeded replay verification:
        Same seed β†’ same state hash at corresponding tick.
        """
        state_dict = {
            "tick_number": self.tick_number,
            "frame_number": self.frame_number,
            "time_ms": self.time_ms,
            "rng_state": self._rng_state,
            "characters": {
                char_id: {
                    "state": char_slot.current_state.value,
                    "frames_elapsed": char_slot.segment_state.frames_elapsed if char_slot.segment_state else 0,
                }
                for char_id, char_slot in self.characters.items()
            }
        }
        
        state_json = json.dumps(state_dict, sort_keys=True)
        return hashlib.sha256(state_json.encode()).hexdigest()[:16]
    
    def reset(self) -> None:
        """Reset loop to initial state (for replay)."""
        LOGGER.info(
            "scheduler.reset.start tick=%s frame=%s registered_chars=%s",
            self.tick_number,
            self.frame_number,
            len(self.characters),
        )
        self.tick_number = 0
        self.frame_number = 0
        self.time_ms = 0.0
        self._rng_state = self.seed
        self.tick_history = []
        
        for char_slot in self.characters.values():
            char_slot.current_state = CharacterState.IDLE
            char_slot.segment_state = None
        LOGGER.info("scheduler.reset.exit")


# ============================================================================
# Deterministic Test Scenarios
# ============================================================================

def two_character_interaction_scenario() -> tuple[DeterministicLoop, List[dict]]:
    """
    Test scenario: Two characters dancing with synchronized transitions.
    
    Returns:
        (loop, motion_frames_per_char)
    """
    loop = DeterministicLoop(fps=30, seed=42)
    
    # Register characters
    loop.register_character("dancer1", "soma", priority=1)
    loop.register_character("dancer2", "soma", priority=1)
    
    # Simulate 2 segments x 30 frames each = 60 frames total
    motion_sequence = [
        {
            "dancer1": {"action": "walk_forward", "frame": i} for i in range(30)
        },
        {
            "dancer2": {"action": "follow", "frame": i} for i in range(30)
        },
    ]
    
    return loop, motion_sequence


def three_character_scenario() -> tuple[DeterministicLoop, List[dict]]:
    """
    Test scenario: Three characters with controlled interactions.
    
    Returns:
        (loop, motion_frames)
    """
    loop = DeterministicLoop(fps=30, seed=43, conflict_policy=ConflictResolutionPolicy.PRIORITY_BASED)
    
    # Register with different priorities
    loop.register_character("leader", "soma", priority=3)
    loop.register_character("follower1", "soma", priority=2)
    loop.register_character("follower2", "soma", priority=1)
    
    motion_sequence = [
        {
            "leader": {"action": "lead", "frame": i},
            "follower1": {"action": "follow", "frame": i},
            "follower2": {"action": "match", "frame": i},
        }
        for i in range(60)
    ]
    
    return loop, motion_sequence


def test_deterministic_replay():
    """
    Verify deterministic replay: same seed produces identical state hashes.
    """
    print("=== Card 3: Deterministic Loop Test ===\n")
    
    # Scenario 1: Two-character deterministic replay
    print("Test 1: Two-character deterministic replay")
    
    loop1, motions1 = two_character_interaction_scenario()
    loop2, motions2 = two_character_interaction_scenario()
    
    hashes1 = []
    hashes2 = []
    
    for tick_num in range(60):
        loop1.advance_tick({})
        loop2.advance_tick({})
        
        hash1 = loop1.get_state_hash()
        hash2 = loop2.get_state_hash()
        
        hashes1.append(hash1)
        hashes2.append(hash2)
    
    if hashes1 == hashes2:
        print("βœ“ Deterministic replay (2-char): PASS")
    else:
        print(f"βœ— Deterministic replay (2-char): FAIL")
        print(f"  Mismatch at frame: {[i for i, (h1, h2) in enumerate(zip(hashes1, hashes2)) if h1 != h2]}")
    
    print()
    
    # Scenario 2: Three-character with priority conflict resolution
    print("Test 2: Three-character priority-based conflict resolution")
    
    loop3, motions3 = three_character_scenario()
    loop4, motions4 = three_character_scenario()
    
    hashes3 = []
    hashes4 = []
    
    for tick_num in range(60):
        loop3.advance_tick({})
        loop4.advance_tick({})
        
        hash3 = loop3.get_state_hash()
        hash4 = loop4.get_state_hash()
        
        hashes3.append(hash3)
        hashes4.append(hash4)
    
    if hashes3 == hashes4:
        print("βœ“ Deterministic replay (3-char): PASS")
    else:
        print(f"βœ— Deterministic replay (3-char): FAIL")
    
    print()
    
    # Scenario 3: Different seed produces different hashes
    print("Test 3: Different seed produces different outcome")
    
    loop_seed42, _ = two_character_interaction_scenario()
    loop_seed99 = DeterministicLoop(fps=30, seed=99)
    loop_seed99.register_character("dancer1", "soma", priority=1)
    loop_seed99.register_character("dancer2", "soma", priority=1)
    
    hashes42 = []
    hashes99 = []
    
    for tick_num in range(30):
        loop_seed42.advance_tick({})
        loop_seed99.advance_tick({})
        
        hashes42.append(loop_seed42.get_state_hash())
        hashes99.append(loop_seed99.get_state_hash())
    
    if hashes42 != hashes99:
        print("βœ“ Different seeds produce different outcomes: PASS")
    else:
        print("βœ— Different seeds should differ: FAIL")
    
    print()
    print("=== All Deterministic Tests Complete ===")


if __name__ == "__main__":
    test_deterministic_replay()