Implement new match video flow with betting calculation

- Remove automatic PLAY_VIDEO_MATCH sending from MatchTimerComponent
- Add MATCH_START handler to GamesThread with betting calculation logic
- Update Message Builder to include result parameter in PLAY_VIDEO_MATCH messages
- Update Qt Player to use match_video.html template for match videos
- Add new match_video.html overlay template for displaying match results

The new flow: Start Games -> MATCH_START -> betting calculation -> PLAY_VIDEO_MATCH with result -> display match video with result overlay
parent 01b4e509
...@@ -11,7 +11,7 @@ from typing import Optional, Dict, Any, List ...@@ -11,7 +11,7 @@ from typing import Optional, Dict, Any, List
from .thread_manager import ThreadedComponent from .thread_manager import ThreadedComponent
from .message_bus import MessageBus, Message, MessageType, MessageBuilder from .message_bus import MessageBus, Message, MessageType, MessageBuilder
from ..database.manager import DatabaseManager from ..database.manager import DatabaseManager
from ..database.models import MatchModel, MatchStatus from ..database.models import MatchModel, MatchStatus, BetDetailModel, MatchOutcomeModel, GameConfigModel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -39,6 +39,7 @@ class GamesThread(ThreadedComponent): ...@@ -39,6 +39,7 @@ class GamesThread(ThreadedComponent):
self.message_bus.subscribe(self.name, MessageType.START_GAME, self._handle_start_game) self.message_bus.subscribe(self.name, MessageType.START_GAME, self._handle_start_game)
self.message_bus.subscribe(self.name, MessageType.SCHEDULE_GAMES, self._handle_schedule_games) self.message_bus.subscribe(self.name, MessageType.SCHEDULE_GAMES, self._handle_schedule_games)
self.message_bus.subscribe(self.name, MessageType.SYSTEM_SHUTDOWN, self._handle_shutdown_message) self.message_bus.subscribe(self.name, MessageType.SYSTEM_SHUTDOWN, self._handle_shutdown_message)
self.message_bus.subscribe(self.name, MessageType.MATCH_START, self._handle_match_start)
# Send ready status # Send ready status
ready_message = MessageBuilder.system_status( ready_message = MessageBuilder.system_status(
...@@ -236,6 +237,31 @@ class GamesThread(ThreadedComponent): ...@@ -236,6 +237,31 @@ class GamesThread(ThreadedComponent):
self._shutdown_event.set() self._shutdown_event.set()
self.game_active = False self.game_active = False
def _handle_match_start(self, message: Message):
"""Handle MATCH_START message and determine match result based on betting"""
try:
fixture_id = message.data.get("fixture_id")
match_id = message.data.get("match_id")
logger.info(f"Processing MATCH_START for fixture {fixture_id}, match {match_id}")
# Calculate result using betting CAP logic
result = self._calculate_match_result(fixture_id, match_id)
# Send PLAY_VIDEO_MATCH with calculated result
self._send_play_video_match(fixture_id, match_id, result)
except Exception as e:
logger.error(f"Failed to handle MATCH_START message: {e}")
# Fallback to random selection if betting calculation fails
try:
fixture_id = message.data.get("fixture_id")
match_id = message.data.get("match_id")
result = self._weighted_random_selection(1.0, 1.0) # Equal weights
self._send_play_video_match(fixture_id, match_id, result)
except Exception as fallback_e:
logger.error(f"Fallback betting calculation also failed: {fallback_e}")
def _process_message(self, message: Message): def _process_message(self, message: Message):
"""Process incoming messages""" """Process incoming messages"""
try: try:
...@@ -1009,6 +1035,219 @@ class GamesThread(ThreadedComponent): ...@@ -1009,6 +1035,219 @@ class GamesThread(ThreadedComponent):
except Exception as e: except Exception as e:
logger.error(f"Failed to unzip ZIP file for match {match_id}: {e}") logger.error(f"Failed to unzip ZIP file for match {match_id}: {e}")
def _calculate_match_result(self, fixture_id: str, match_id: int) -> str:
"""Calculate match result based on betting patterns and CAP logic"""
try:
session = self.db_manager.get_session()
try:
# Get UNDER/OVER coefficients from match outcomes
under_coeff, over_coeff = self._get_fixture_coefficients(fixture_id, session)
if under_coeff is None or over_coeff is None:
logger.warning(f"No coefficients found for fixture {fixture_id}, using random selection")
return self._weighted_random_selection(1.0, 1.0)
# Calculate payouts for both outcomes
under_payout = self._calculate_payout(match_id, 'UNDER', under_coeff, session)
over_payout = self._calculate_payout(match_id, 'OVER', over_coeff, session)
# Calculate total payin
total_payin = self._calculate_total_payin(match_id, session)
# Get redistribution CAP percentage
cap_percentage = self._get_redistribution_cap()
# Check CAP logic
max_payout = max(under_payout, over_payout)
cap_threshold = total_payin * (cap_percentage / 100.0)
logger.info(f"Match {match_id}: UNDER payout={under_payout:.2f}, OVER payout={over_payout:.2f}, total_payin={total_payin:.2f}, CAP={cap_percentage}%, threshold={cap_threshold:.2f}")
if max_payout > cap_threshold:
# CAP exceeded - select outcome with lower payout to minimize losses
if under_payout <= over_payout:
result = 'UNDER'
logger.info(f"CAP exceeded, selecting UNDER (lower payout: {under_payout:.2f} vs {over_payout:.2f})")
else:
result = 'OVER'
logger.info(f"CAP exceeded, selecting OVER (lower payout: {over_payout:.2f} vs {under_payout:.2f})")
else:
# CAP not exceeded - use weighted random selection
result = self._weighted_random_selection(under_coeff, over_coeff)
logger.info(f"CAP not exceeded, using weighted random: {result}")
return result
finally:
session.close()
except Exception as e:
logger.error(f"Failed to calculate match result: {e}")
# Fallback to random
return self._weighted_random_selection(1.0, 1.0)
def _calculate_payout(self, match_id: int, outcome: str, coefficient: float, session) -> float:
"""Calculate payout for an outcome"""
try:
# Get total bets for this outcome on this match
total_bet_amount = session.query(
BetDetailModel.amount
).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == outcome,
BetDetailModel.active_status == True
).all()
total_amount = sum(bet.amount for bet in total_bet_amount) if total_bet_amount else 0.0
payout = total_amount * coefficient
return payout
except Exception as e:
logger.error(f"Failed to calculate payout for {outcome}: {e}")
return 0.0
def _calculate_total_payin(self, match_id: int, session) -> float:
"""Calculate total payin (sum of all UNDER + OVER bets)"""
try:
total_under = session.query(
BetDetailModel.amount
).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'UNDER',
BetDetailModel.active_status == True
).all()
total_over = session.query(
BetDetailModel.amount
).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'OVER',
BetDetailModel.active_status == True
).all()
under_amount = sum(bet.amount for bet in total_under) if total_under else 0.0
over_amount = sum(bet.amount for bet in total_over) if total_over else 0.0
return under_amount + over_amount
except Exception as e:
logger.error(f"Failed to calculate total payin: {e}")
return 0.0
def _get_fixture_coefficients(self, fixture_id: str, session) -> tuple:
"""Get UNDER/OVER coefficients from fixture/match outcomes"""
try:
# Get match outcomes for this fixture
outcomes = session.query(MatchOutcomeModel).filter(
MatchOutcomeModel.fixture_id == fixture_id,
MatchOutcomeModel.active_status == True
).all()
under_coeff = None
over_coeff = None
for outcome in outcomes:
if outcome.outcome_type == 'UNDER':
under_coeff = outcome.coefficient
elif outcome.outcome_type == 'OVER':
over_coeff = outcome.coefficient
return under_coeff, over_coeff
except Exception as e:
logger.error(f"Failed to get fixture coefficients: {e}")
return None, None
def _get_redistribution_cap(self) -> float:
"""Get redistribution CAP percentage from configuration"""
try:
session = self.db_manager.get_session()
try:
# Get global CAP configuration
cap_config = session.query(GameConfigModel).filter_by(
config_key='redistribution_cap'
).first()
if cap_config:
cap_value = cap_config.get_typed_value()
if isinstance(cap_value, (int, float)) and 10 <= cap_value <= 100:
return float(cap_value)
else:
logger.warning(f"Invalid CAP value: {cap_value}, using default 70%")
else:
logger.debug("No CAP configuration found, using default 70%")
return 70.0 # Default CAP
finally:
session.close()
except Exception as e:
logger.error(f"Failed to get redistribution CAP: {e}")
return 70.0
def _weighted_random_selection(self, under_coeff: float, over_coeff: float) -> str:
"""Weighted random selection based on inverse coefficients"""
try:
import random
# Higher coefficients get lower probability (inverse weighting)
under_weight = 1.0 / under_coeff if under_coeff > 0 else 1.0
over_weight = 1.0 / over_coeff if over_coeff > 0 else 1.0
total_weight = under_weight + over_weight
if total_weight == 0:
# Fallback to equal weights
under_weight = over_weight = 1.0
total_weight = 2.0
# Generate random number
rand = random.uniform(0, total_weight)
if rand < under_weight:
return 'UNDER'
else:
return 'OVER'
except Exception as e:
logger.error(f"Failed to perform weighted random selection: {e}")
# Fallback to 50/50
import random
return 'UNDER' if random.random() < 0.5 else 'OVER'
def _send_play_video_match(self, fixture_id: str, match_id: int, result: str):
"""Send PLAY_VIDEO_MATCH message with calculated result"""
try:
# Get video filename based on result
video_filename = self._get_match_video_filename(match_id, result)
# Send PLAY_VIDEO_MATCH message with result
play_message = MessageBuilder.play_video_match(
sender=self.name,
fixture_id=fixture_id,
match_id=match_id,
video_filename=video_filename,
result=result
)
self.message_bus.publish(play_message)
logger.info(f"Sent PLAY_VIDEO_MATCH for fixture {fixture_id}, match {match_id}, result {result}, video {video_filename}")
except Exception as e:
logger.error(f"Failed to send PLAY_VIDEO_MATCH: {e}")
def _get_match_video_filename(self, match_id: int, result: str) -> str:
"""Get appropriate video filename based on result"""
try:
# For now, use result.mp4 (UNDER.mp4 or OVER.mp4)
# In future, this could be more sophisticated based on match data
return f"{result}.mp4"
except Exception as e:
logger.error(f"Failed to get match video filename: {e}")
return f"{result}.mp4" # Fallback
def _cleanup(self): def _cleanup(self):
"""Perform cleanup operations""" """Perform cleanup operations"""
try: try:
......
...@@ -619,16 +619,19 @@ class MessageBuilder: ...@@ -619,16 +619,19 @@ class MessageBuilder:
) )
@staticmethod @staticmethod
def play_video_match(sender: str, match_id: int, video_filename: str, fixture_id: Optional[str] = None) -> Message: def play_video_match(sender: str, match_id: int, video_filename: str, fixture_id: Optional[str] = None, result: str = None) -> Message:
"""Create PLAY_VIDEO_MATCH message""" """Create PLAY_VIDEO_MATCH message"""
return Message( data = {
type=MessageType.PLAY_VIDEO_MATCH,
sender=sender,
data={
"match_id": match_id, "match_id": match_id,
"video_filename": video_filename, "video_filename": video_filename,
"fixture_id": fixture_id "fixture_id": fixture_id
} }
if result is not None:
data["result"] = result
return Message(
type=MessageType.PLAY_VIDEO_MATCH,
sender=sender,
data=data
) )
@staticmethod @staticmethod
......
...@@ -2871,11 +2871,22 @@ class QtVideoPlayer(QObject): ...@@ -2871,11 +2871,22 @@ class QtVideoPlayer(QObject):
match_id = message.data.get("match_id") match_id = message.data.get("match_id")
video_filename = message.data.get("video_filename") video_filename = message.data.get("video_filename")
fixture_id = message.data.get("fixture_id") fixture_id = message.data.get("fixture_id")
result = message.data.get("result") # Extract result parameter
if not match_id or not video_filename: if not match_id or not video_filename:
logger.error("Missing match_id or video_filename in PLAY_VIDEO_MATCH message") logger.error("Missing match_id or video_filename in PLAY_VIDEO_MATCH message")
return return
# Use result to determine video filename if not provided or to validate
if result:
expected_filename = f"{result}.mp4"
if video_filename != expected_filename:
logger.warning(f"Video filename mismatch: expected {expected_filename}, got {video_filename}")
video_filename = expected_filename # Override with result-based filename
logger.info(f"Match result: {result}, using video: {video_filename}")
else:
logger.warning("No result provided in PLAY_VIDEO_MATCH message")
# Stop the current intro video loop and template rotation # Stop the current intro video loop and template rotation
if self.window and hasattr(self.window, 'media_player'): if self.window and hasattr(self.window, 'media_player'):
logger.info("Stopping intro video loop and template rotation") logger.info("Stopping intro video loop and template rotation")
...@@ -2906,13 +2917,20 @@ class QtVideoPlayer(QObject): ...@@ -2906,13 +2917,20 @@ class QtVideoPlayer(QObject):
# Play the match video (no looping) # Play the match video (no looping)
if self.window: if self.window:
# Update overlay with result information
overlay_data = {
"title": f"Match {match_id}",
"subtitle": f"Result: {result}" if result else "Live Action",
"result": result
}
self.window.play_video( self.window.play_video(
str(match_video_path), str(match_video_path),
template_data={"title": f"Match {match_id}", "subtitle": "Live Action"}, template_data=overlay_data,
template_name="news_template", template_name="match_video.html",
loop_data=None # No looping for match videos loop_data=None # No looping for match videos
) )
logger.info(f"Match video started: {video_filename}") logger.info(f"Match video started: {video_filename} with result: {result}")
else: else:
logger.error("No window available for match video playback") logger.error("No window available for match video playback")
else: else:
......
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Match Video Overlay</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: transparent !important;
background-color: transparent !important;
overflow: hidden;
width: 100vw;
height: 100vh;
position: relative;
}
/* Debug indicator */
body::before {
content: 'Match Video Overlay v1.0';
position: absolute;
top: 5px;
left: 5px;
color: rgba(255,255,255,0.5);
font-size: 10px;
z-index: 9999;
}
/* Top right timer */
.timer-container {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px 15px;
border-radius: 8px;
font-size: 24px;
font-weight: bold;
font-family: 'Courier New', monospace;
border: 2px solid rgba(255, 255, 255, 0.3);
z-index: 1000;
min-width: 120px;
text-align: center;
}
/* Bottom info bar */
.info-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
border-top: 2px solid rgba(255, 255, 255, 0.2);
padding: 15px;
z-index: 1000;
}
.fighter-names {
color: white;
font-size: 28px;
font-weight: bold;
text-align: center;
margin-bottom: 5px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.venue-info {
color: rgba(255, 255, 255, 0.9);
font-size: 20px;
text-align: center;
font-style: italic;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
}
/* Result indicator (optional) */
.result-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 0, 0, 0.9);
color: white;
padding: 20px 40px;
border-radius: 15px;
font-size: 36px;
font-weight: bold;
text-align: center;
border: 3px solid rgba(255, 255, 255, 0.8);
z-index: 1001;
display: none;
animation: resultPulse 2s ease-in-out infinite;
}
@keyframes resultPulse {
0% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.1); }
100% { transform: translate(-50%, -50%) scale(1); }
}
/* Responsive design */
@media (max-width: 1200px) {
.fighter-names {
font-size: 24px;
}
.venue-info {
font-size: 18px;
}
.timer-container {
font-size: 20px;
padding: 8px 12px;
}
}
@media (max-width: 800px) {
.fighter-names {
font-size: 20px;
}
.venue-info {
font-size: 16px;
}
.timer-container {
font-size: 18px;
padding: 6px 10px;
top: 10px;
right: 10px;
}
.info-bar {
padding: 10px;
}
}
</style>
</head>
<body>
<!-- Timer in top right -->
<div class="timer-container" id="matchTimer">
00:00
</div>
<!-- Result indicator (shown briefly at end) -->
<div class="result-indicator" id="resultIndicator">
<div id="resultText">RESULT</div>
</div>
<!-- Bottom info bar -->
<div class="info-bar">
<div class="fighter-names" id="fighterNames">Loading fighters...</div>
<div class="venue-info" id="venueInfo">Loading venue...</div>
</div>
<script>
// Global variables
let overlayData = {};
let matchData = null;
let videoDuration = 0;
let timerInterval = null;
let currentTime = 0;
let webServerBaseUrl = 'http://127.0.0.1:5001';
// Function to update overlay data (called by Qt WebChannel)
function updateOverlayData(data) {
console.log('Match video overlay received data:', data);
overlayData = data || {};
// Update web server base URL if provided
if (data && data.webServerBaseUrl) {
webServerBaseUrl = data.webServerBaseUrl;
console.log('Updated web server base URL:', webServerBaseUrl);
}
// Extract match data
if (data && data.match) {
matchData = data.match;
updateMatchInfo();
}
// Extract video duration if provided
if (data && data.video_duration) {
videoDuration = parseFloat(data.video_duration) || 0;
console.log('Video duration set to:', videoDuration, 'seconds');
}
// Extract result if provided
if (data && data.result) {
showResult(data.result);
}
}
// Update match information display
function updateMatchInfo() {
if (!matchData) return;
const fighterNames = document.getElementById('fighterNames');
const venueInfo = document.getElementById('venueInfo');
// Get fighter names
const fighter1 = matchData.fighter1_township || matchData.fighter1 || 'Fighter 1';
const fighter2 = matchData.fighter2_township || matchData.fighter2 || 'Fighter 2';
fighterNames.textContent = `${fighter1} vs ${fighter2}`;
// Get venue
const venue = matchData.venue_kampala_township || matchData.venue || 'Venue TBA';
venueInfo.textContent = venue;
console.log('Updated match info:', { fighters: `${fighter1} vs ${fighter2}`, venue: venue });
}
// Start match timer
function startTimer() {
if (timerInterval) {
clearInterval(timerInterval);
}
currentTime = 0;
updateTimerDisplay();
timerInterval = setInterval(() => {
currentTime++;
updateTimerDisplay();
// Check if we've reached video duration
if (videoDuration > 0 && currentTime >= videoDuration) {
stopTimer();
onVideoEnd();
}
}, 1000);
}
// Stop match timer
function stopTimer() {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
}
// Update timer display
function updateTimerDisplay() {
const minutes = Math.floor(currentTime / 60);
const seconds = currentTime % 60;
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
document.getElementById('matchTimer').textContent = timeString;
}
// Handle video end
function onVideoEnd() {
console.log('Video ended, showing result');
if (overlayData && overlayData.result) {
showResult(overlayData.result);
}
}
// Show result indicator
function showResult(result) {
const resultIndicator = document.getElementById('resultIndicator');
const resultText = document.getElementById('resultText');
resultText.textContent = result.toUpperCase();
resultIndicator.style.display = 'block';
// Hide after 5 seconds
setTimeout(() => {
resultIndicator.style.display = 'none';
}, 5000);
}
// Fetch match data from API
async function fetchMatchData(matchId) {
try {
console.log('Fetching match data for ID:', matchId);
const response = await fetch(`${webServerBaseUrl}/api/matches/${matchId}`);
const data = await response.json();
if (data.success && data.match) {
matchData = data.match;
updateMatchInfo();
console.log('Fetched match data:', matchData);
} else {
console.warn('Failed to fetch match data:', data);
}
} catch (error) {
console.error('Error fetching match data:', error);
}
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('Match video overlay initialized');
// Start timer immediately (will be synced with video)
startTimer();
// If we have match data from overlay, use it
if (overlayData && overlayData.match) {
updateMatchInfo();
} else if (overlayData && overlayData.match_id) {
// Try to fetch match data
fetchMatchData(overlayData.match_id);
}
});
// Qt WebChannel initialization
if (typeof QWebChannel !== 'undefined') {
new QWebChannel(qt.webChannelTransport, function(channel) {
console.log('WebChannel initialized for match video overlay');
// Connect to overlay object if available
if (channel.objects.overlay) {
channel.objects.overlay.dataChanged.connect(function(data) {
updateOverlayData(data);
});
// Get initial data
if (channel.objects.overlay.getCurrentData) {
channel.objects.overlay.getCurrentData(function(data) {
updateOverlayData(data);
});
}
}
});
}
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
stopTimer();
});
</script>
<!-- Required scripts for Qt WebChannel communication -->
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script src="overlay://overlay.js"></script>
</body>
</html>
\ No newline at end of file
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