Implement continuous game operation with match recycling

- Modified _monitor_game_state to create new matches from old completed ones instead of stopping when fixture completes
- Enhanced _initialize_new_fixture to create fixtures from old matches when no new fixtures available
- Added helper methods for match selection and recycling:
  * _select_random_completed_matches(): Selects random completed matches
  * _create_matches_from_old_matches(): Creates new matches in existing fixture
  * _create_new_fixture_from_old_matches(): Creates new fixture from recycled matches
- Added comprehensive documentation explaining game flow and human intervention points
- Game now runs continuously until machine restart, recycling completed matches to maintain betting opportunities
parent 6b64015f
# MbetterClient Game Logic Documentation
## Architectural Overview
The MbetterClient implements a continuous betting game system for boxing matches with sophisticated game flow management, CAP-based result calculation, and automatic fixture recycling.
### Core Components
1. **GamesThread**: Main game orchestration and state management
2. **MatchTimer**: Handles match timing and progression
3. **MessageBus**: Inter-component communication system
4. **Database Layer**: SQLAlchemy ORM for data persistence
5. **Video Player**: Qt-based video playback system
6. **Web Dashboard**: Real-time monitoring and administration
### Key Design Patterns
- **Message-Driven Architecture**: All components communicate via typed messages
- **State Machine Pattern**: Matches progress through defined status transitions
- **Observer Pattern**: Components subscribe to relevant message types
- **Transaction Safety**: Database operations wrapped in proper transactions
## Game Flow Documentation
### Phase 1: System Initialization
**Trigger**: Application startup
**Human Intervention**: None required
```
Application Start → GamesThread.initialize() → MessageBus registration →
Subscribe to message types → Send SYSTEM_STATUS(ready) → Begin monitoring loop
```
**Code Flow**:
```python
def initialize(self) -> bool:
# Register with message bus
self.message_queue = self.message_bus.register_component(self.name)
# Subscribe to messages
self.message_bus.subscribe(self.name, MessageType.START_GAME, self._handle_start_game)
# ... other subscriptions
# Send ready status
ready_message = MessageBuilder.system_status(...)
self.message_bus.publish(ready_message)
```
### Phase 2: Fixture Activation
**Trigger**: START_GAME message received
**Human Intervention**: None (automatic), but can be triggered via dashboard
#### Decision Tree for START_GAME Processing
```
START_GAME message received
├── Fixture ID provided?
│ ├── Yes → Check if fixture terminal (all matches done/cancelled/failed)?
│ │ ├── Yes → Discard message, send GAME_STATUS(discarded)
│ │ └── No → Activate provided fixture
│ └── No → Check today's fixtures all terminal?
│ ├── Yes → Discard message, send GAME_STATUS(discarded)
│ └── Find active today fixture?
│ ├── Yes → Activate fixture
│ └── No → Initialize new fixture
```
#### New Fixture Initialization Logic
```
Initialize New Fixture
├── Search fixtures with start_time = NULL
│ ├── Found → Set start_time = now(), status = 'scheduled'
│ └── Not found → Create recycled fixture
│ ├── Select 5 random completed matches
│ │ ├── Success → Create fixture "recycle_{uuid}"
│ │ └── No matches → Cannot initialize (new installation scenario)
│ └── Copy match data and outcomes
```
**Code Implementation**:
```python
def _handle_start_game(self, message: Message):
fixture_id = message.data.get("fixture_id")
if fixture_id:
if self._is_fixture_all_terminal(fixture_id):
# Discard message
return
self._activate_fixture(fixture_id, message)
else:
# Complex logic for finding/creating fixtures
# ... (see code for full implementation)
```
### Phase 3: Match Scheduling and Betting Setup
**Trigger**: Fixture activation
**Human Intervention**: Configuration via dashboard (betting mode, CAP settings)
#### Betting Mode Configuration
**Database Configuration**:
- `betting_mode`: 'all_bets_on_start' or 'one_bet_at_a_time'
- `redistribution_cap`: Percentage (10-100, default 70%)
#### Match Status Transitions
```
All matches: pending → scheduled
├── Betting mode = 'all_bets_on_start'
│ └── All scheduled → bet (simultaneous betting)
└── Betting mode = 'one_bet_at_a_time'
└── First scheduled → bet (sequential betting)
```
**Code Flow**:
```python
def _schedule_and_apply_betting_logic(self, fixture_id: str):
betting_mode = self._get_betting_mode_config()
# Change all non-terminal to scheduled
# Apply betting mode logic
if betting_mode == 'all_bets_on_start':
# All scheduled → bet
else:
# Only first scheduled → bet
```
### Phase 4: Match Execution Loop
**Trigger**: Match timer progression
**Human Intervention**: None (fully automated)
#### 4.1 Match Start Event
**Trigger**: Timer sends MATCH_START message
**Result Calculation with CAP Logic**:
```
Calculate Match Result
├── Get UNDER/OVER coefficients from fixture
├── Calculate potential payouts:
│ ├── UNDER payout = under_amount × under_coefficient
│ └── OVER payout = over_amount × over_coefficient
├── Calculate total payin = under_amount + over_amount
├── CAP threshold = total_payin × (cap_percentage / 100)
├── Max payout > CAP threshold?
│ ├── Yes → Select outcome with lower payout (minimize losses)
│ └── No → Weighted random selection (1/coefficient weighting)
└── Send PLAY_VIDEO_MATCH message
```
**CAP Logic Implementation**:
```python
def _calculate_match_result(self, fixture_id: str, match_id: int) -> str:
under_coeff, over_coeff = self._get_fixture_coefficients(fixture_id, session)
under_payout = self._calculate_payout(match_id, 'UNDER', under_coeff, session)
over_payout = self._calculate_payout(match_id, 'OVER', over_coeff, session)
total_payin = self._calculate_total_payin(match_id, session)
cap_threshold = total_payin * (cap_percentage / 100.0)
if max(under_payout, over_payout) > cap_threshold:
# Select lower payout outcome
return 'UNDER' if under_payout <= over_payout else 'OVER'
else:
# Weighted random selection
return self._weighted_random_selection(under_coeff, over_coeff)
```
#### 4.2 Video Playback Phase
**Trigger**: PLAY_VIDEO_MATCH message received by video player
```
Video Player Flow:
PLAY_VIDEO_MATCH received → Unzip match ZIP file (if needed) →
Play appropriate video (UNDER.mp4 or OVER.mp4) →
Video completes → Send PLAY_VIDEO_MATCH_DONE
```
#### 4.3 Result Extraction Phase
**Trigger**: PLAY_VIDEO_MATCH_DONE message
**Complex Result Extraction**:
```
Result Extraction Process:
├── Set match status = 'ingame'
├── Get all result options (excluding UNDER/OVER)
├── For each result option:
│ ├── Calculate payout based on associations and coefficients
│ └── Filter results below CAP threshold
├── Eligible results available?
│ ├── Yes → Weighted random selection
│ └── No → Select lowest payout result
├── Update all bet results (win/lost/pending)
├── Collect match statistics
└── Send PLAY_VIDEO_RESULT message
```
**Bet Result Update Logic**:
```python
def _update_bet_results(self, match_id: int, selected_result: str, session):
# Handle UNDER/OVER bets
if selected_result in ['UNDER', 'OVER']:
# UNDER/OVER winner gets payout, other loses
else:
# Selected result wins, all others lose
# Calculate win_amount = bet_amount × coefficient
```
#### 4.4 Match Completion
**Trigger**: MATCH_DONE message
```
Match Completion:
├── Update match status = 'done'
├── Wait 2 seconds (configurable)
├── Send NEXT_MATCH message
└── Timer advances to next match
```
### Phase 5: Fixture Completion (Modified Continuous Operation)
**Previous Behavior**: Fixture completes → Game stops → Wait for server fixture
**New Behavior**: Fixture completes → Recycle matches → Continue game
#### Continuous Game Loop Implementation
```
Fixture Completion Detected:
├── All matches terminal (done/cancelled/failed/paused)?
│ ├── Yes → Select 5 random completed matches
│ │ ├── Success → Create new matches in current fixture
│ │ │ └── Copy match data, outcomes, coefficients
│ │ └── No matches available → Stop game (new installation)
│ └── No → Continue monitoring
└── Game continues until system shutdown
```
**Code Implementation**:
```python
def _monitor_game_state(self):
active_count = session.query(MatchModel).filter(
MatchModel.fixture_id == self.current_fixture_id,
MatchModel.status.in_(['pending', 'scheduled', 'bet', 'ingame']),
MatchModel.active_status == True
).count()
if active_count == 0:
# Fixture completed - recycle matches
old_matches = self._select_random_completed_matches(5, session)
if old_matches:
self._create_matches_from_old_matches(self.current_fixture_id, old_matches, session)
# Continue game loop
else:
# No old matches - stop game
self.game_active = False
```
## Human Intervention Points
### 1. System Startup
- **Action**: Start application
- **Intervention Level**: None (automatic)
- **Monitoring**: Check system logs for initialization status
### 2. Fixture Management
- **Action**: Upload/download fixtures
- **Intervention Level**: Optional (automatic downloads available)
- **Tools**: Web dashboard fixture management
- **Monitoring**: Dashboard shows fixture status and availability
### 3. Betting Configuration
- **Action**: Configure betting parameters
- **Intervention Level**: Administrative
- **Tools**: Web dashboard configuration
- **Parameters**:
- Betting mode (all_bets_on_start / one_bet_at_a_time)
- Redistribution CAP percentage
- Result options and associations
### 4. System Monitoring
- **Action**: Monitor game status and statistics
- **Intervention Level**: Optional monitoring
- **Tools**: Web dashboard with real-time updates
- **Alerts**: Automatic notifications for system events
### 5. Emergency Controls
- **Action**: Manual game stop/start
- **Intervention Level**: Administrative override
- **Tools**: Dashboard controls
- **Use Cases**: System maintenance, issue resolution
### 6. System Shutdown
- **Action**: Stop application
- **Intervention Level**: Required
- **Process**: Clean shutdown preserves state
## Edge Cases and Error Handling
### 1. New Installation Scenario
**Condition**: No completed matches in database
**Behavior**:
- Cannot create recycled fixtures
- Game stops with appropriate logging
- Requires server fixture download to resume operation
**Code Handling**:
```python
if not old_matches:
logger.warning("No old completed matches found - cannot create new matches")
self.game_active = False
# Send completion message
```
### 2. Insufficient Completed Matches
**Condition**: Fewer than 5 completed matches available
**Behavior**:
- Uses all available completed matches
- Logs warning about insufficient matches
- Continues operation with available matches
### 3. Database Connection Issues
**Condition**: Database unavailable during critical operations
**Behavior**:
- Operations wrapped in try/catch blocks
- Automatic transaction rollback on errors
- Graceful degradation with comprehensive logging
- System attempts retry for non-critical operations
### 4. Message Bus Failures
**Condition**: Message delivery failures
**Behavior**:
- Messages may be lost but system continues
- Critical operations have timeout/retry logic
- Status updates sent to dashboard for monitoring
- System remains operational despite individual message losses
### 5. Video Player Disconnection
**Condition**: Video player component unavailable
**Behavior**:
- Match timer continues running
- Ingame matches marked as failed after timeout
- System attempts to continue with next matches
- Comprehensive logging for troubleshooting
### 6. Timer Component Failures
**Condition**: Match timer stops unexpectedly
**Behavior**:
- System detects timer failure via status checks
- Ingame matches changed to failed status
- Checks for other active fixtures before discarding messages
- May discard START_GAME messages if no alternatives available
### 7. Configuration Errors
**Condition**: Invalid or missing configuration
**Behavior**:
- Falls back to sensible defaults
- Logs warnings about configuration issues
- Continues operation with default values
- Administrative intervention required for permanent fix
## Configuration Parameters
### Database Configuration (game_config table)
- `betting_mode`: 'all_bets_on_start' or 'one_bet_at_a_time'
- `redistribution_cap`: Integer 10-100 (default: 70)
### System Constants
- `RECYCLE_MATCH_COUNT`: 5 (matches to create in recycled fixtures)
- `FIXTURE_PREFIX`: 'recycle_' (prefix for auto-generated fixtures)
- `TIMER_SLEEP_INTERVAL`: 0.1 seconds (monitoring loop frequency)
- `MATCH_COMPLETION_DELAY`: 2 seconds (delay before next match)
### Message Types
#### Incoming Messages
- `START_GAME`: Initiate or resume game operation
- `SCHEDULE_GAMES`: Change pending matches to scheduled status
- `SYSTEM_SHUTDOWN`: Initiate clean system shutdown
- `MATCH_START`: Trigger result calculation for specific match
- `PLAY_VIDEO_MATCH_DONE`: Video playback completed, trigger extraction
- `MATCH_DONE`: Match completed, advance to next match
- `GAME_UPDATE`: Process game state update information
#### Outgoing Messages
- `GAME_STATUS`: Status updates and command responses
- `PLAY_VIDEO_MATCH`: Video playback command with result
- `PLAY_VIDEO_RESULT`: Result display command
- `NEXT_MATCH`: Advance to next match in sequence
- `START_INTRO`: Fixture introduction video trigger
- `CUSTOM`: Dashboard notifications and updates
## Database Schema Integration
### Core Game Tables
- `matches`: Match data, status, and metadata
- `match_outcomes`: Betting coefficients and outcome data
- `bets`: Bet transaction records
- `bets_details`: Individual bet details and results
- `extraction_stats`: Match completion statistics
### Configuration Tables
- `game_config`: System-wide configuration settings
- `result_options`: Available result types for extraction
- `available_bets`: Configurable betting options
- `extraction_associations`: Result-to-bet mappings
### Supporting Tables
- `users`: User authentication and permissions
- `sessions`: Web dashboard session management
- `log_entries`: System logging and audit trail
## Performance Considerations
### Monitoring Loop Frequency
- 0.1 second intervals balance responsiveness with CPU usage
- Database queries optimized with proper indexing
- Message processing designed for low latency
### Memory Management
- Database sessions properly scoped and closed
- Large result sets handled with pagination where appropriate
- Temporary files cleaned up after video extraction
### Transaction Safety
- Critical operations wrapped in database transactions
- Automatic rollback on errors prevents data corruption
- Proper error logging for troubleshooting
This documentation provides comprehensive coverage of the game logic from both architectural and operational perspectives, including all possible scenarios, edge cases, and human intervention points.
\ No newline at end of file
...@@ -375,22 +375,27 @@ class GamesThread(ThreadedComponent): ...@@ -375,22 +375,27 @@ class GamesThread(ThreadedComponent):
).count() ).count()
if active_count == 0: if active_count == 0:
logger.info(f"All matches completed for fixture {self.current_fixture_id}") logger.info(f"All matches completed for fixture {self.current_fixture_id} - creating new matches from old completed ones")
self.game_active = False
# Send game completed message # Instead of stopping the game, create 5 new matches from old completed matches
old_matches = self._select_random_completed_matches(5, session)
if old_matches:
self._create_matches_from_old_matches(self.current_fixture_id, old_matches, session)
logger.info(f"Created 5 new matches in fixture {self.current_fixture_id} from old completed matches")
else:
logger.warning("No old completed matches found - cannot create new matches")
# If no old matches available, stop the game
self.game_active = False
completed_message = Message( completed_message = Message(
type=MessageType.GAME_STATUS, type=MessageType.GAME_STATUS,
sender=self.name, sender=self.name,
data={ data={
"status": "completed", "status": "completed_no_old_matches",
"fixture_id": self.current_fixture_id, "fixture_id": self.current_fixture_id,
"timestamp": time.time() "timestamp": time.time()
} }
) )
self.message_bus.publish(completed_message) self.message_bus.publish(completed_message)
# Reset current fixture
self.current_fixture_id = None self.current_fixture_id = None
finally: finally:
...@@ -652,21 +657,19 @@ class GamesThread(ThreadedComponent): ...@@ -652,21 +657,19 @@ class GamesThread(ThreadedComponent):
return None return None
def _initialize_new_fixture(self) -> Optional[str]: def _initialize_new_fixture(self) -> Optional[str]:
"""Initialize a new fixture by finding the first one with no start_time set""" """Initialize a new fixture by finding the first one with no start_time set, or create one from old matches"""
try: try:
session = self.db_manager.get_session() session = self.db_manager.get_session()
try: try:
# Find the first fixture with no start_time set # First, try to find the first fixture with no start_time set
fixtures_no_start_time = session.query(MatchModel.fixture_id).filter( fixtures_no_start_time = session.query(MatchModel.fixture_id).filter(
MatchModel.start_time.is_(None), MatchModel.start_time.is_(None),
MatchModel.active_status == True MatchModel.active_status == True
).distinct().order_by(MatchModel.created_at.asc()).all() ).distinct().order_by(MatchModel.created_at.asc()).all()
if not fixtures_no_start_time: if fixtures_no_start_time:
return None
fixture_id = fixtures_no_start_time[0].fixture_id fixture_id = fixtures_no_start_time[0].fixture_id
logger.info(f"Initializing new fixture: {fixture_id}") logger.info(f"Initializing existing fixture with no start_time: {fixture_id}")
# Set start_time to now for all matches in this fixture # Set start_time to now for all matches in this fixture
now = datetime.utcnow() now = datetime.utcnow()
...@@ -683,6 +686,21 @@ class GamesThread(ThreadedComponent): ...@@ -683,6 +686,21 @@ class GamesThread(ThreadedComponent):
session.commit() session.commit()
return fixture_id return fixture_id
# No fixtures with no start_time found - create a new fixture from old completed matches
logger.info("No fixtures with no start_time found - creating new fixture from old completed matches")
old_matches = self._select_random_completed_matches(5, session)
if old_matches:
fixture_id = self._create_new_fixture_from_old_matches(old_matches, session)
if fixture_id:
logger.info(f"Created new fixture {fixture_id} from old completed matches")
return fixture_id
else:
logger.warning("Failed to create new fixture from old matches")
return None
else:
logger.warning("No old completed matches found - cannot create new fixture")
return None
finally: finally:
session.close() session.close()
...@@ -1700,6 +1718,129 @@ class GamesThread(ThreadedComponent): ...@@ -1700,6 +1718,129 @@ class GamesThread(ThreadedComponent):
except Exception as e: except Exception as e:
logger.error(f"Failed to send NEXT_MATCH: {e}") logger.error(f"Failed to send NEXT_MATCH: {e}")
def _select_random_completed_matches(self, count: int, session) -> List[MatchModel]:
"""Select random completed matches from the database"""
try:
# Get all completed matches (status = 'done')
completed_matches = session.query(MatchModel).filter(
MatchModel.status == 'done',
MatchModel.active_status == True
).all()
if len(completed_matches) < count:
logger.warning(f"Only {len(completed_matches)} completed matches found, requested {count}")
return completed_matches
# Select random matches
import random
selected_matches = random.sample(completed_matches, count)
logger.info(f"Selected {len(selected_matches)} random completed matches")
return selected_matches
except Exception as e:
logger.error(f"Failed to select random completed matches: {e}")
return []
def _create_matches_from_old_matches(self, fixture_id: str, old_matches: List[MatchModel], session):
"""Create new matches in the fixture by copying from old completed matches"""
try:
now = datetime.utcnow()
match_number = 1
for old_match in old_matches:
# Create a new match based on the old one
new_match = MatchModel(
match_number=match_number,
fighter1_township=old_match.fighter1_township,
fighter2_township=old_match.fighter2_township,
venue_kampala_township=old_match.venue_kampala_township,
start_time=now,
status='scheduled',
fixture_id=fixture_id,
filename=old_match.filename,
file_sha1sum=old_match.file_sha1sum,
active_status=True,
zip_filename=old_match.zip_filename,
zip_sha1sum=old_match.zip_sha1sum,
zip_upload_status='completed', # Assume ZIP is already available
fixture_active_time=int(now.timestamp())
)
session.add(new_match)
session.flush() # Get the ID
# Copy match outcomes
for outcome in old_match.outcomes:
new_outcome = MatchOutcomeModel(
match_id=new_match.id,
column_name=outcome.column_name,
float_value=outcome.float_value
)
session.add(new_outcome)
logger.debug(f"Created new match #{match_number} from old match #{old_match.match_number}")
match_number += 1
session.commit()
logger.info(f"Created {len(old_matches)} new matches in fixture {fixture_id}")
except Exception as e:
logger.error(f"Failed to create matches from old matches: {e}")
session.rollback()
raise
def _create_new_fixture_from_old_matches(self, old_matches: List[MatchModel], session) -> Optional[str]:
"""Create a new fixture with matches copied from old completed matches"""
try:
# Generate a unique fixture ID
import uuid
fixture_id = f"recycle_{uuid.uuid4().hex[:8]}"
now = datetime.utcnow()
match_number = 1
for old_match in old_matches:
# Create a new match based on the old one
new_match = MatchModel(
match_number=match_number,
fighter1_township=old_match.fighter1_township,
fighter2_township=old_match.fighter2_township,
venue_kampala_township=old_match.venue_kampala_township,
start_time=now,
status='scheduled',
fixture_id=fixture_id,
filename=old_match.filename,
file_sha1sum=old_match.file_sha1sum,
active_status=True,
zip_filename=old_match.zip_filename,
zip_sha1sum=old_match.zip_sha1sum,
zip_upload_status='completed', # Assume ZIP is already available
fixture_active_time=int(now.timestamp())
)
session.add(new_match)
session.flush() # Get the ID
# Copy match outcomes
for outcome in old_match.outcomes:
new_outcome = MatchOutcomeModel(
match_id=new_match.id,
column_name=outcome.column_name,
float_value=outcome.float_value
)
session.add(new_outcome)
logger.debug(f"Created match #{match_number} in new fixture {fixture_id} from old match #{old_match.match_number}")
match_number += 1
session.commit()
logger.info(f"Created new fixture {fixture_id} with {len(old_matches)} matches from old completed matches")
return fixture_id
except Exception as e:
logger.error(f"Failed to create new fixture from old matches: {e}")
session.rollback()
return None
def _cleanup(self): def _cleanup(self):
"""Perform cleanup operations""" """Perform cleanup operations"""
try: try:
......
# MbetterClient v1.2.11
Cross-platform multimedia client application
## Installation
1. Extract this package to your desired location
2. Run the executable file
3. The application will create necessary configuration files on first run
## System Requirements
- **Operating System**: Linux 6.16.3+deb14-amd64
- **Architecture**: x86_64
- **Memory**: 512 MB RAM minimum, 1 GB recommended
- **Disk Space**: 100 MB free space
## Configuration
The application stores its configuration and database in:
- **Windows**: `%APPDATA%\MbetterClient`
- **macOS**: `~/Library/Application Support/MbetterClient`
- **Linux**: `~/.config/MbetterClient`
## Web Interface
By default, the web interface is available at: http://localhost:5001
Default login credentials:
- Username: admin
- Password: admin
**Please change the default password after first login.**
## Support
For support and documentation, please visit: https://git.nexlab.net/mbetter/mbetterc
## Version Information
- Version: 1.2.11
- Build Date: zeiss
- Platform: Linux-6.16.3+deb14-amd64-x86_64-with-glibc2.41
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment