Tournament Lifecycle
Tournaments progress through distinct states from creation to completion. Understanding this lifecycle helps you implement proper timing, state management, and user experience for tournament features.
Tournament States
Tournaments move through these primary states:
1. Upcoming (Pre-Registration)
The tournament is announced but not yet started.
Characteristics:
- Tournament details are visible
- Players can view rules and prizes
- Registration/joining may be available
- No score submissions allowed
- Countdown to start time displayed
Common Actions:
- Display tournament information
- Show join/registration buttons
- Display countdown timer
- Send reminder notifications
2. Active (In Progress)
The tournament is running and accepting scores.
Characteristics:
- Score submissions are open
- Real-time rankings available
- Players can join (if registration is still open)
- Live time remaining displayed
- Competition is ongoing
Common Actions:
- Enable score submission
- Show live rankings
- Display time remaining
- Handle score validation
- Update participant counts
3. Ended (Closed)
The tournament has finished but results are being processed.
Characteristics:
- No more score submissions accepted
- Tournament is technically complete
- Results may be processing
- Final rankings being calculated
- Prize distribution in progress
Common Actions:
- Disable score submission
- Show "tournament ended" status
- Display "results processing" message
- Prepare for results display
4. Results (Completed)
Final results are available and tournament is archived.
Characteristics:
- Final rankings are published
- Prize winners announced
- Tournament is archived
- Historical data available
- No further modifications possible
Common Actions:
- Display final rankings
- Show prize winners
- Enable results sharing
- Provide tournament statistics
- Archive tournament data
State Transitions
stateDiagram-v2
[*] --> Upcoming
Upcoming --> Active : Tournament starts
Active --> Ended : Tournament ends
Ended --> Results : Results processed
Results --> [*] : Tournament archived
Upcoming --> Active : Early start
Active --> Ended : Force end
Ended --> Results : Immediate processing
Timing Management
Tournament Schedule
Each tournament has defined timing parameters:
interface TournamentTiming {
announceTime: number; // When tournament is announced
registrationOpen: number; // When players can start joining
startTime: number; // When tournament officially starts
joinDeadline: number; // Last time players can join
endTime: number; // When tournament ends
resultsTime: number; // When results are published
}
Time-Based Logic
function getTournamentState(tournament) {
const now = Date.now();
if (now < tournament.startTime) {
return 'upcoming';
} else if (now >= tournament.startTime && now < tournament.endTime) {
return 'active';
} else if (now >= tournament.endTime && now < tournament.resultsTime) {
return 'ended';
} else if (now >= tournament.resultsTime) {
return 'results';
}
return 'unknown';
}
function getTimeRemaining(tournament) {
const now = Date.now();
const state = getTournamentState(tournament);
switch (state) {
case 'upcoming':
return tournament.startTime - now;
case 'active':
return tournament.endTime - now;
case 'ended':
return tournament.resultsTime - now;
default:
return 0;
}
}
Implementation Examples
Tournament State Manager
class TournamentStateManager {
constructor() {
this.tournaments = new Map();
this.stateChangeCallbacks = new Map();
this.timers = new Map();
}
// Add tournament to monitor
addTournament(tournament) {
this.tournaments.set(tournament.id, tournament);
this.scheduleStateCheck(tournament.id);
}
// Remove tournament from monitoring
removeTournament(tournamentId) {
this.tournaments.delete(tournamentId);
this.clearTimer(tournamentId);
}
// Get current state of tournament
getState(tournamentId) {
const tournament = this.tournaments.get(tournamentId);
if (!tournament) return null;
return getTournamentState(tournament);
}
// Register callback for state changes
onStateChange(tournamentId, callback) {
if (!this.stateChangeCallbacks.has(tournamentId)) {
this.stateChangeCallbacks.set(tournamentId, []);
}
this.stateChangeCallbacks.get(tournamentId).push(callback);
}
// Schedule periodic state checks
scheduleStateCheck(tournamentId) {
const tournament = this.tournaments.get(tournamentId);
if (!tournament) return;
// Clear existing timer
this.clearTimer(tournamentId);
// Calculate next check time
const nextCheck = this.calculateNextCheckTime(tournament);
const delay = Math.max(0, nextCheck - Date.now());
// Schedule timer
const timerId = setTimeout(() => {
this.checkTournamentState(tournamentId);
}, delay);
this.timers.set(tournamentId, timerId);
}
// Check and handle state changes
checkTournamentState(tournamentId) {
const tournament = this.tournaments.get(tournamentId);
if (!tournament) return;
const currentState = this.getState(tournamentId);
const previousState = tournament._lastState;
// Handle state change
if (currentState !== previousState) {
tournament._lastState = currentState;
this.handleStateChange(tournamentId, currentState, previousState);
}
// Schedule next check
this.scheduleStateCheck(tournamentId);
}
// Handle state change notifications
handleStateChange(tournamentId, newState, oldState) {
console.log(`Tournament ${tournamentId} changed from ${oldState} to ${newState}`);
// Notify registered callbacks
const callbacks = this.stateChangeCallbacks.get(tournamentId) || [];
callbacks.forEach(callback => {
try {
callback(newState, oldState, tournamentId);
} catch (error) {
console.error('State change callback error:', error);
}
});
// Handle automatic actions based on state
this.handleStateActions(tournamentId, newState);
}
// Handle automatic actions for each state
handleStateActions(tournamentId, state) {
const tournament = this.tournaments.get(tournamentId);
if (!tournament) return;
switch (state) {
case 'active':
this.onTournamentStart(tournamentId);
break;
case 'ended':
this.onTournamentEnd(tournamentId);
break;
case 'results':
this.onResultsAvailable(tournamentId);
break;
}
}
// Calculate when to next check tournament state
calculateNextCheckTime(tournament) {
const now = Date.now();
const state = getTournamentState(tournament);
switch (state) {
case 'upcoming':
// Check at start time
return tournament.startTime;
case 'active':
// Check at end time
return tournament.endTime;
case 'ended':
// Check at results time
return tournament.resultsTime || tournament.endTime + 300000; // 5 minutes after end
default:
return now + 60000; // Check again in 1 minute
}
}
// Clear scheduled timer
clearTimer(tournamentId) {
const timerId = this.timers.get(tournamentId);
if (timerId) {
clearTimeout(timerId);
this.timers.delete(tournamentId);
}
}
// Tournament start handler
onTournamentStart(tournamentId) {
console.log(`Tournament ${tournamentId} has started!`);
// Notify players
this.notifyTournamentStart(tournamentId);
// Update UI
this.updateTournamentUI(tournamentId, 'active');
// Start live updates
this.startLiveUpdates(tournamentId);
}
// Tournament end handler
onTournamentEnd(tournamentId) {
console.log(`Tournament ${tournamentId} has ended!`);
// Stop score submissions
this.disableScoreSubmission(tournamentId);
// Show processing message
this.showResultsProcessing(tournamentId);
// Prepare for results
this.prepareResults(tournamentId);
}
// Results available handler
onResultsAvailable(tournamentId) {
console.log(`Results available for tournament ${tournamentId}`);
// Load and display results
this.loadTournamentResults(tournamentId);
// Notify participants
this.notifyResultsAvailable(tournamentId);
// Enable sharing
this.enableResultsSharing(tournamentId);
}
}
// Usage
const stateManager = new TournamentStateManager();
// Add tournament to monitor
stateManager.addTournament({
id: 'weekly-challenge-001',
name: 'Weekly Challenge',
startTime: Date.now() + 3600000, // 1 hour from now
endTime: Date.now() + 86400000, // 24 hours from now
resultsTime: Date.now() + 87000000 // 24h 5m from now
});
// Listen for state changes
stateManager.onStateChange('weekly-challenge-001', (newState, oldState) => {
console.log(`State changed: ${oldState} → ${newState}`);
// Update UI based on new state
updateTournamentDisplay(newState);
});
Countdown Timer Component
class TournamentCountdown {
constructor(tournamentId, container) {
this.tournamentId = tournamentId;
this.container = container;
this.isActive = false;
this.animationFrame = null;
}
// Start countdown for tournament
start(tournament) {
this.tournament = tournament;
this.isActive = true;
this.update();
}
// Stop countdown
stop() {
this.isActive = false;
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame);
this.animationFrame = null;
}
}
// Update countdown display
update() {
if (!this.isActive) return;
const now = Date.now();
const state = getTournamentState(this.tournament);
const timeRemaining = getTimeRemaining(this.tournament);
if (timeRemaining <= 0) {
this.handleCountdownEnd(state);
return;
}
// Update display
this.renderCountdown(timeRemaining, state);
// Schedule next update
this.animationFrame = requestAnimationFrame(() => this.update());
}
// Render countdown display
renderCountdown(timeRemaining, state) {
const timeData = this.parseTimeRemaining(timeRemaining);
const message = this.getCountdownMessage(state);
this.container.innerHTML = `
<div class="tournament-countdown ${state}">
<div class="countdown-message">${message}</div>
<div class="countdown-timer">
${timeData.days > 0 ? `<span class="time-unit days">${timeData.days}d</span>` : ''}
<span class="time-unit hours">${timeData.hours}h</span>
<span class="time-unit minutes">${timeData.minutes}m</span>
<span class="time-unit seconds">${timeData.seconds}s</span>
</div>
${this.getProgressBar(timeRemaining, state)}
</div>
`;
}
// Parse time remaining into components
parseTimeRemaining(milliseconds) {
const totalSeconds = Math.floor(milliseconds / 1000);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return { days, hours, minutes, seconds };
}
// Get countdown message based on state
getCountdownMessage(state) {
const messages = {
'upcoming': 'Tournament starts in:',
'active': 'Tournament ends in:',
'ended': 'Results available in:',
'results': 'Tournament completed'
};
return messages[state] || 'Time remaining:';
}
// Get progress bar for tournament
getProgressBar(timeRemaining, state) {
if (state !== 'active') return '';
const totalTime = this.tournament.endTime - this.tournament.startTime;
const elapsed = totalTime - timeRemaining;
const progress = (elapsed / totalTime) * 100;
return `
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress}%"></div>
</div>
`;
}
// Handle countdown end
handleCountdownEnd(state) {
this.stop();
switch (state) {
case 'upcoming':
this.container.innerHTML = '<div class="tournament-status started">Tournament Started!</div>';
this.triggerTournamentStart();
break;
case 'active':
this.container.innerHTML = '<div class="tournament-status ended">Tournament Ended!</div>';
this.triggerTournamentEnd();
break;
case 'ended':
this.container.innerHTML = '<div class="tournament-status results">Results Available!</div>';
this.triggerResultsAvailable();
break;
default:
this.container.innerHTML = '<div class="tournament-status completed">Tournament Completed</div>';
}
}
// Trigger tournament start
triggerTournamentStart() {
// Refresh tournament data
this.refreshTournamentData();
// Notify listeners
this.onTournamentStart?.();
}
// Trigger tournament end
triggerTournamentEnd() {
// Disable score submission
this.disableScoreSubmission();
// Show processing message
this.showProcessingMessage();
// Notify listeners
this.onTournamentEnd?.();
}
// Trigger results available
triggerResultsAvailable() {
// Load results
this.loadResults();
// Notify listeners
this.onResultsAvailable?.();
}
// Refresh tournament data
refreshTournamentData() {
MoitribeSDK('my-game-id', 'groupTournamentData', {
tournamentid: this.tournamentId,
callback: (result) => {
if (result.success) {
this.tournament = result.tournament;
// Restart countdown with updated data
this.start(this.tournament);
}
}
});
}
}
// Usage
const countdown = new TournamentCountdown('weekly-challenge-001',
document.getElementById('countdown-container'));
// Start countdown
countdown.start(tournamentData);
// Listen for events
countdown.onTournamentStart = () => {
console.log('Tournament has started!');
enableScoreSubmission();
};
countdown.onTournamentEnd = () => {
console.log('Tournament has ended!');
disableScoreSubmission();
};
countdown.onResultsAvailable = () => {
console.log('Results are available!');
loadTournamentResults();
};
State-Based UI Management
class TournamentUIManager {
constructor(tournamentId) {
this.tournamentId = tournamentId;
this.currentState = null;
this.tournamentData = null;
}
// Initialize UI with tournament data
initialize(tournament) {
this.tournamentData = tournament;
const state = getTournamentState(tournament);
this.updateUI(state);
}
// Update UI based on tournament state
updateUI(state) {
if (this.currentState === state) return;
this.currentState = state;
// Hide all state-specific elements
this.hideAllStateElements();
// Show elements for current state
this.showStateElements(state);
// Update common elements
this.updateCommonElements(state);
// Trigger state-specific actions
this.handleStateActions(state);
}
// Hide all state-specific UI elements
hideAllStateElements() {
const elements = [
'.upcoming-ui',
'.active-ui',
'.ended-ui',
'.results-ui'
];
elements.forEach(selector => {
const element = document.querySelector(selector);
if (element) {
element.style.display = 'none';
}
});
}
// Show UI elements for specific state
showStateElements(state) {
const selector = `.${state}-ui`;
const element = document.querySelector(selector);
if (element) {
element.style.display = 'block';
}
}
// Update common UI elements
updateCommonElements(state) {
// Update status badge
const statusBadge = document.getElementById('tournament-status');
if (statusBadge) {
statusBadge.textContent = state.toUpperCase();
statusBadge.className = `status-badge ${state}`;
}
// Update action buttons
this.updateActionButtons(state);
// Update navigation
this.updateNavigation(state);
}
// Update action buttons based on state
updateActionButtons(state) {
const actionsContainer = document.getElementById('tournament-actions');
if (!actionsContainer) return;
switch (state) {
case 'upcoming':
actionsContainer.innerHTML = `
<button id="join-btn" class="btn-primary" onclick="joinTournament('${this.tournamentId}')">
Join Tournament
</button>
<button class="btn-secondary" onclick="setReminder('${this.tournamentId}')">
Set Reminder
</button>
`;
break;
case 'active':
actionsContainer.innerHTML = `
<button id="submit-score-btn" class="btn-primary" onclick="showScoreSubmission()">
Submit Score
</button>
<button class="btn-secondary" onclick="viewRankings()">
View Rankings
</button>
`;
break;
case 'ended':
actionsContainer.innerHTML = `
<button class="btn-secondary" disabled>
Processing Results...
</button>
`;
break;
case 'results':
actionsContainer.innerHTML = `
<button class="btn-primary" onclick="viewResults()">
View Results
</button>
<button class="btn-secondary" onclick="shareResults()">
Share Results
</button>
`;
break;
}
}
// Update navigation menu
updateNavigation(state) {
const navItems = document.querySelectorAll('.tournament-nav-item');
navItems.forEach(item => {
const itemState = item.dataset.state;
if (itemState === state) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
}
// Handle state-specific actions
handleStateActions(state) {
switch (state) {
case 'upcoming':
this.startCountdown(this.tournamentData.startTime);
this.enableNotifications();
break;
case 'active':
this.startCountdown(this.tournamentData.endTime);
this.enableScoreSubmission();
this.startLiveUpdates();
break;
case 'ended':
this.stopCountdown();
this.disableScoreSubmission();
this.showProcessingMessage();
break;
case 'results':
this.stopCountdown();
this.loadResults();
break;
}
}
// Start countdown timer
startCountdown(targetTime) {
const countdown = new TournamentCountdown(this.tournamentId,
document.getElementById('countdown-container'));
countdown.start({
...this.tournamentData,
endTime: targetTime
});
}
// Stop countdown timer
stopCountdown() {
const container = document.getElementById('countdown-container');
container.innerHTML = '';
}
// Enable score submission
enableScoreSubmission() {
const submitBtn = document.getElementById('submit-score-btn');
if (submitBtn) {
submitBtn.disabled = false;
}
}
// Disable score submission
disableScoreSubmission() {
const submitBtn = document.getElementById('submit-score-btn');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.textContent = 'Tournament Ended';
}
}
// Show processing message
showProcessingMessage() {
const container = document.getElementById('tournament-content');
container.innerHTML = `
<div class="processing-message">
<div class="spinner"></div>
<h2>Processing Results</h2>
<p>Final rankings are being calculated. Please wait...</p>
</div>
`;
}
// Load tournament results
loadResults() {
MoitribeSDK('my-game-id', 'groupTournamentResults', {
tournamentid: this.tournamentId,
callback: (result) => {
if (result.success) {
this.displayResults(result);
} else {
this.showResultsError(result.msg);
}
}
});
}
// Display tournament results
displayResults(results) {
const container = document.getElementById('tournament-content');
container.innerHTML = `
<div class="tournament-results">
<!-- Results content here -->
</div>
`;
}
}
// Usage
const uiManager = new TournamentUIManager('weekly-challenge-001');
uiManager.initialize(tournamentData);
Best Practices
State Persistence
Maintain tournament state across page refreshes:
class TournamentStatePersistence {
constructor() {
this.storageKey = 'tournament-states';
}
// Save tournament state
saveState(tournamentId, state, data) {
const states = this.loadAllStates();
states[tournamentId] = {
state: state,
data: data,
timestamp: Date.now()
};
localStorage.setItem(this.storageKey, JSON.stringify(states));
}
// Load tournament state
loadState(tournamentId) {
const states = this.loadAllStates();
return states[tournamentId] || null;
}
// Load all tournament states
loadAllStates() {
const stored = localStorage.getItem(this.storageKey);
return stored ? JSON.parse(stored) : {};
}
// Clear old states (older than 7 days)
clearOldStates() {
const states = this.loadAllStates();
const cutoff = Date.now() - (7 * 24 * 60 * 60 * 1000); // 7 days ago
Object.keys(states).forEach(tournamentId => {
if (states[tournamentId].timestamp < cutoff) {
delete states[tournamentId];
}
});
localStorage.setItem(this.storageKey, JSON.stringify(states));
}
}
Error Recovery
Handle state transitions gracefully:
class TournamentStateRecovery {
constructor(stateManager) {
this.stateManager = stateManager;
this.recoveryAttempts = new Map();
}
// Handle state transition errors
handleStateError(tournamentId, error, expectedState) {
console.error(`State transition error for ${tournamentId}:`, error);
const attempts = this.recoveryAttempts.get(tournamentId) || 0;
if (attempts < 3) {
// Retry state transition
this.recoveryAttempts.set(tournamentId, attempts + 1);
setTimeout(() => {
this.retryStateTransition(tournamentId, expectedState);
}, 2000 * (attempts + 1)); // Exponential backoff
} else {
// Max retries reached, refresh tournament data
this.refreshTournamentData(tournamentId);
}
}
// Retry state transition
retryStateTransition(tournamentId, expectedState) {
this.stateManager.checkTournamentState(tournamentId);
}
// Refresh tournament data from server
refreshTournamentData(tournamentId) {
MoitribeSDK('my-game-id', 'groupTournamentData', {
tournamentid: tournamentId,
callback: (result) => {
if (result.success) {
this.recoveryAttempts.delete(tournamentId);
this.stateManager.addTournament(result.tournament);
}
}
});
}
}
Next Steps
Understanding tournament lifecycle helps with:
- Get Metadata - Find tournaments in different states
- Join Tournament - Join at appropriate times
- Submit Score - Submit during active periods
- Get Results - View results when available
Related topics:
- Tournament Overview - General tournament concepts
- Authentication - Required for participation
- Real-Time Multiplayer - Live competition features