Home > Technical Reference > Data Models > Tournament


Tournament Data Models

Complete reference for tournament-related entities in Archery Apprentice. These models support both local (offline) and remote (online) tournaments with full participant management, leaderboard tracking, and offline sync capabilities.

Overview

Core Entities: 3 (TournamentEntity, TournamentParticipantEntity, TournamentScoreCache) Location: shared:database module Package: com.archeryapprentice.database.entities.tournament

Entity Relationships

TournamentEntity (tournament metadata)
  └── TournamentParticipantEntity (participants)
        └── Scores tracked via Round.tournamentId

TournamentScoreCache (offline sync)
  └── Cached scores for remote tournaments

Tournament Types

TypeDescriptionSync RequiredUse Case
LocalOffline tournamentNoClub events, practice competitions
RemoteOnline tournamentYesNational competitions, remote participation
HybridLocal with optional syncOptionalLocal event with online leaderboard

TournamentEntity

The TournamentEntity represents a complete tournament configuration and metadata.

File: TournamentEntity.kt Table: TournamentEntity Total Fields: 36

Field Categories

1. Identity & Metadata (5 fields)

FieldTypeDescriptionExample
idStringPrimary key (UUID or custom ID)"spring-tourney-2025", "uuid-abc-123"
nameStringTournament name"Spring Championship 2025"
descriptionString?Tournament details"Annual club championship, 70m outdoor"
locationString?Venue or geographic location"Central Archery Range, Chicago"
organizerIdString?User ID of tournament creator"user123"

2. Creator Information (2 fields)

FieldTypeDescriptionExample
createdByString?Creator username"Chris"
creatorEmailString?Creator email"chris@example.com"

Purpose: Contact information for tournament organizer

3. Status Management (1 field)

FieldTypeDescriptionAllowed Values
statusTournamentStatusCurrent tournament stateOPEN, IN_PROGRESS, COMPLETED, CANCELLED

Status Workflow:

OPEN → IN_PROGRESS → COMPLETED
  ↓
CANCELLED

Status Definitions:

  • OPEN - Registration open, tournament not started
  • IN_PROGRESS - Tournament active, participants shooting
  • COMPLETED - Tournament finished, final results available
  • CANCELLED - Tournament abandoned/cancelled

4. Round Format (6 fields)

FieldTypeDescriptionExample
numEndsIntEnds per round6, 10, 12
numArrowsIntArrows per end3, 6
distanceDistanceShooting distance"70 meters", "50 yards"
targetSizeTargetSizeTarget face size"122cm", "80cm"
scoringSystemScoringSystemScoring method10-ring, 6-ring, 5-zone
totalRoundsInt?Number of rounds in tournament3, 5, 10

Round Format Notes:

  • All participants shoot same format
  • Format locked when tournament starts (status → IN_PROGRESS)
  • Compatible with standard Round entity

5. Timestamps (3 fields)

FieldTypeDescriptionPurpose
startTimeInstant?Tournament start timeScheduling, countdown
endTimeInstant?Tournament end timeDeadline enforcement
createdAtInstantCreation timestampSorting, history

Timestamp Behavior:

  • createdAt set on creation
  • startTime optional, can be scheduled future event
  • endTime optional, enforces submission deadline

6. Access Control (4 fields)

FieldTypeDescriptionExample
isPublicBooleanPublic vs private tournamenttrue (anyone can join), false (invite only)
joinCodeString?Access code for joining"SPRING2025", "ABC123"
maxParticipantsInt?Maximum participants50, 100, null (unlimited)
requireApprovalBooleanRequire organizer approval to jointrue, false

Access Control Modes:

Public Tournament:

val publicTourney = TournamentEntity(
    isPublic = true,
    joinCode = null,           // No code required
    requireApproval = false,   // Auto-join
    maxParticipants = null     // Unlimited
)

Private Tournament:

val privateTourney = TournamentEntity(
    isPublic = false,
    joinCode = "SECRET2025",   // Code required
    requireApproval = true,    // Manual approval
    maxParticipants = 20       // Limited slots
)

7. Tournament Settings (2 fields)

FieldTypeDescriptionDefault
allowSpectatorsBooleanAllow non-participants to viewtrue
useSettingsDisplayNamesBooleanUse Settings usernames vs custom namestrue

Display Name Behavior:

  • true: Participants show as Settings.userName
  • false: Participants can set custom tournament display names

8. Leaderboard & Scoring (4 fields)

FieldTypeDescriptionCalculation
currentRoundIntActive round number1, 2, 3
topScoreInt?Highest score across all roundsCached for leaderboard
averageScoreDouble?Mean score of all participantsCached for statistics
totalParticipantsIntParticipant countCached count

Why Cache These?

  • Avoid expensive aggregations on every leaderboard view
  • Update on round completion or participant action
  • Quick tournament statistics display

9. Sync & Local/Remote (3 fields)

FieldTypeDescriptionExample
isLocalBooleanLocal (offline) vs remote (online)true (offline), false (online)
needsSyncBooleanHas unsynced changestrue (needs upload), false (synced)
lastSyncedAtInstant?Last successful sync timeFor conflict resolution

Sync Workflow:

Local Tournament:

val localTourney = TournamentEntity(
    isLocal = true,
    needsSync = false,      // No sync needed
    lastSyncedAt = null     // Never synced
)

Remote Tournament with Offline Support:

// 1. Create remote tournament
val remoteTourney = TournamentEntity(
    isLocal = false,
    needsSync = false,
    lastSyncedAt = Clock.System.now()
)
 
// 2. Participant shoots offline
// Scores stored in TournamentScoreCache
// needsSync = true
 
// 3. Device comes online
// Sync cached scores to server
// needsSync = false, lastSyncedAt = now

10. Rules & Configuration (6 fields)

FieldTypeDescriptionExample
eliminationRoundsInt?Number of elimination rounds2, 3
advancementCriteriaString?How participants advance"Top 50%", "Top 10 scores"
tiebreakingRulesString?How ties are resolved"X-count", "Closest to center"
customRulesString?Additional tournament rules"Must use barebow equipment"
registrationDeadlineInstant?Last moment to registerEnforce cutoff
roundDeadlinesMap<Int, Instant>?Per-round submission deadlines{1: deadline1, 2: deadline2}

Rule Enforcement:

  • Rules stored as strings for flexibility
  • Enforcement typically manual or UI-level
  • Can be parsed for automated validation

Complete TournamentEntity Example

val championship = TournamentEntity(
    id = "spring-champ-2025",
    name = "Spring Championship 2025",
    description = "Annual club championship, outdoor 70m",
    location = "Central Archery Range",
    organizerId = "user123",
    createdBy = "Chris",
    creatorEmail = "chris@archeryclub.com",
 
    // Status
    status = TournamentStatus.OPEN,
 
    // Round format
    numEnds = 12,
    numArrows = 6,
    distance = Distance.fromString("70 meters"),
    targetSize = TargetSize.fromString("122cm"),
    scoringSystem = ScoringSystem.TEN_RING,
    totalRounds = 3,
 
    // Timestamps
    startTime = Clock.System.now().plus(7.days),
    endTime = Clock.System.now().plus(14.days),
    createdAt = Clock.System.now(),
 
    // Access control
    isPublic = true,
    joinCode = null,
    maxParticipants = 50,
    requireApproval = false,
 
    // Settings
    allowSpectators = true,
    useSettingsDisplayNames = true,
 
    // Leaderboard
    currentRound = 1,
    topScore = null,
    averageScore = null,
    totalParticipants = 0,
 
    // Sync
    isLocal = false,  // Remote tournament
    needsSync = false,
    lastSyncedAt = Clock.System.now(),
 
    // Rules
    eliminationRounds = 2,
    advancementCriteria = "Top 50% advance to finals",
    tiebreakingRules = "X-count, then closest arrow to center",
    customRules = null,
    registrationDeadline = Clock.System.now().plus(6.days),
    roundDeadlines = mapOf(
        1 to Clock.System.now().plus(8.days),
        2 to Clock.System.now().plus(11.days),
        3 to Clock.System.now().plus(14.days)
    )
)

TournamentParticipantEntity

Represents a participant in a tournament, tracking their progress and ranking.

File: TournamentParticipantEntity.kt Table: TournamentParticipant

Fields

FieldTypeDescriptionExample
idLongPrimary key, auto-increment1, 2, 3
tournamentIdStringForeign key → TournamentEntity(id)"spring-champ-2025"
userIdStringParticipant user ID"user456"
displayNameStringParticipant display name"Sarah Johnson"
currentRoundIntCurrent/latest round completed1, 2, 3
totalScoreIntAggregate score across all rounds1680, 1750
rankingInt?Current leaderboard position1, 2, 3, null (unranked)
statusParticipantStatusParticipation statusACTIVE, WITHDRAWN, DISQUALIFIED
registeredAtInstantRegistration timestampSorting, history
lastRoundSubmittedInstant?Last score submission timeTrack activity

Relationships

Parent: TournamentEntity (Many-to-One)

  • Foreign Key: TournamentParticipant.tournamentId → TournamentEntity.id
  • Cascade: ON DELETE CASCADE (deleting tournament deletes all participants)

Children: Round entities (via Round.tournamentId and Round.participantId)

Participant Status

ParticipantStatus Values:

  • ACTIVE - Currently participating in tournament
  • WITHDRAWN - Voluntarily withdrew from tournament
  • DISQUALIFIED - Removed by organizer (rules violation, etc.)
  • COMPLETED - Finished all rounds (optional status)

Usage Example

val participant = TournamentParticipantEntity(
    tournamentId = "spring-champ-2025",
    userId = "user456",
    displayName = "Sarah Johnson",
    currentRound = 2,          // Completed 2 rounds
    totalScore = 1120,         // Sum of round 1 + round 2
    ranking = 3,               // 3rd place
    status = ParticipantStatus.ACTIVE,
    registeredAt = Clock.System.now().minus(5.days),
    lastRoundSubmitted = Clock.System.now().minus(1.days)
)

Queries

Get tournament leaderboard:

@Query("""
    SELECT * FROM TournamentParticipant
    WHERE tournamentId = :tournamentId
    AND status = 'ACTIVE'
    ORDER BY totalScore DESC
""")
suspend fun getLeaderboard(tournamentId: String): List<TournamentParticipantEntity>

Update participant ranking:

@Query("""
    UPDATE TournamentParticipant
    SET ranking = :ranking
    WHERE id = :participantId
""")
suspend fun updateRanking(participantId: Long, ranking: Int)

Get participant’s rounds:

@Query("""
    SELECT * FROM Round
    WHERE tournamentId = :tournamentId
    AND participantId = :participantId
    ORDER BY tournamentRoundNumber ASC
""")
suspend fun getParticipantRounds(tournamentId: String, participantId: String): List<RoundEntity>

TournamentScoreCache

Caches scores for offline tournament participation, syncing when connection available.

File: TournamentScoreCacheEntity.kt Table: TournamentScoreCache

Fields

FieldTypeDescriptionExample
idLongPrimary key, auto-increment1, 2, 3
tournamentIdStringTournament reference"spring-champ-2025"
participantIdStringParticipant user ID"user456"
roundNumberIntWhich round these scores are for1, 2, 3
scoresListArrow scores (JSON)[10, 9, 9, 8, 10, 9, ...]
lastUpdatedInstantLast modification timeFor conflict resolution
syncStatusSyncStatusSync statePENDING, SYNCED, FAILED

Purpose

Offline Tournament Participation:

  1. Participant shoots offline (no network)
  2. Scores saved to TournamentScoreCache
  3. When online, cached scores sync to server
  4. Server updates leaderboard
  5. Cache entry marked SYNCED or deleted

Sync Status

SyncStatus Values:

  • PENDING - Not yet synced to server
  • SYNCED - Successfully uploaded
  • FAILED - Sync attempt failed (will retry)

Usage Example

// 1. Participant shoots offline
val scoreCache = TournamentScoreCacheEntity(
    tournamentId = "spring-champ-2025",
    participantId = "user456",
    roundNumber = 2,
    scores = listOf(10, 9, 9, 8, 10, 9, 8, 7, 10, 9, ...),  // All arrow scores
    lastUpdated = Clock.System.now(),
    syncStatus = SyncStatus.PENDING
)
scoreCacheDao.insert(scoreCache)
 
// 2. Device comes online
val pendingScores = scoreCacheDao.getPendingScores()
 
// 3. Sync to server
for (cache in pendingScores) {
    try {
        tournamentApi.submitScores(cache)
        scoreCacheDao.updateStatus(cache.id, SyncStatus.SYNCED)
        // Optionally delete after successful sync
        scoreCacheDao.delete(cache.id)
    } catch (e: Exception) {
        scoreCacheDao.updateStatus(cache.id, SyncStatus.FAILED)
    }
}

Queries

Get pending sync items:

@Query("""
    SELECT * FROM TournamentScoreCache
    WHERE syncStatus = 'PENDING'
    ORDER BY lastUpdated ASC
""")
suspend fun getPendingScores(): List<TournamentScoreCacheEntity>

Get cached scores for tournament:

@Query("""
    SELECT * FROM TournamentScoreCache
    WHERE tournamentId = :tournamentId
    AND participantId = :participantId
    ORDER BY roundNumber ASC
""")
suspend fun getCachedScores(tournamentId: String, participantId: String): List<TournamentScoreCacheEntity>

Type Converter

// List<Int> ↔ JSON String
@TypeConverter
fun fromIntList(list: List<Int>): String = Json.encodeToString(list)
 
@TypeConverter
fun toIntList(value: String): List<Int> = Json.decodeFromString(value)

Relationships & Data Flow

Tournament → Participant → Rounds

TournamentEntity
  ├── TournamentParticipant (user1)
  │     ├── Round (round 1)
  │     ├── Round (round 2)
  │     └── Round (round 3)
  ├── TournamentParticipant (user2)
  │     ├── Round (round 1)
  │     └── Round (round 2)
  └── TournamentParticipant (user3)
        └── Round (round 1)

Foreign Keys:

  • TournamentParticipant.tournamentId → TournamentEntity.id (CASCADE DELETE)
  • Round.tournamentId → TournamentEntity.id (No FK, but logical relationship)
  • Round.participantId matches TournamentParticipant.userId

Offline Sync Flow

1. User shoots offline
   ↓
2. Save to TournamentScoreCache (PENDING)
   ↓
3. Device goes online
   ↓
4. Sync cached scores to server
   ↓
5. Server updates TournamentParticipant.totalScore
   ↓
6. Recalculate rankings
   ↓
7. Mark cache SYNCED or delete

Tournament Lifecycle

Creating a Tournament

// 1. Create tournament entity
val tournament = TournamentEntity(
    id = generateTournamentId(),
    name = "My Tournament",
    status = TournamentStatus.OPEN,
    numEnds = 6,
    numArrows = 6,
    distance = Distance.fromString("50 meters"),
    targetSize = TargetSize.fromString("122cm"),
    scoringSystem = ScoringSystem.TEN_RING,
    isLocal = false,  // Remote tournament
    createdAt = Clock.System.now()
)
tournamentDao.insert(tournament)

Joining a Tournament

// 1. User requests to join
if (tournament.requireApproval) {
    // Send join request to organizer
    joinRequestDao.insert(JoinRequest(tournamentId, userId))
} else {
    // Auto-join
    val participant = TournamentParticipantEntity(
        tournamentId = tournament.id,
        userId = currentUserId,
        displayName = settingsDao.getUserName(),
        currentRound = 0,
        totalScore = 0,
        ranking = null,
        status = ParticipantStatus.ACTIVE,
        registeredAt = Clock.System.now()
    )
    participantDao.insert(participant)
}

Submitting a Round

// 1. Create round with tournament reference
val round = RoundEntity(
    roundName = "Spring Championship - Round 2",
    tournamentId = tournament.id,
    tournamentRoundNumber = 2,
    numEnds = tournament.numEnds,
    numArrows = tournament.numArrows,
    distance = tournament.distance,
    targetSize = tournament.targetSize,
    scoringSystem = tournament.scoringSystem,
    status = RoundStatus.IN_PROGRESS
)
roundDao.insert(round)
 
// 2. Score the round normally
 
// 3. On completion, update participant
val participant = participantDao.getByUserId(tournament.id, currentUserId)
participantDao.update(participant.copy(
    currentRound = 2,
    totalScore = participant.totalScore + round.totalScore,
    lastRoundSubmitted = Clock.System.now()
))
 
// 4. Recalculate rankings
updateTournamentRankings(tournament.id)

Calculating Rankings

suspend fun updateTournamentRankings(tournamentId: String) {
    // Get all active participants, sorted by score
    val participants = participantDao.getLeaderboard(tournamentId)
 
    // Assign rankings
    participants.forEachIndexed { index, participant ->
        participantDao.updateRanking(participant.id, index + 1)
    }
 
    // Update tournament leaderboard cache
    tournamentDao.updateLeaderboardCache(
        tournamentId = tournamentId,
        topScore = participants.firstOrNull()?.totalScore,
        averageScore = participants.map { it.totalScore }.average()
    )
}

Local vs Remote Tournaments

Local Tournament (Offline)

Characteristics:

  • No network required
  • All data stored locally
  • No sync needed
  • Ideal for club events

Example:

val localTourney = TournamentEntity(
    name = "Club Practice Tournament",
    isLocal = true,
    needsSync = false,
    lastSyncedAt = null
)

Remote Tournament (Online)

Characteristics:

  • Network connection required (or cached offline)
  • Scores sync to central server
  • Real-time leaderboard updates
  • Ideal for national/international events

Example:

val remoteTourney = TournamentEntity(
    name = "National Championship",
    isLocal = false,
    needsSync = false,  // Initially synced
    lastSyncedAt = Clock.System.now()
)

Hybrid: Remote with Offline Support

Workflow:

  1. Online: User joins remote tournament, downloads metadata
  2. Offline: User shoots round, scores cached locally
  3. Online: Scores sync to server, leaderboard updates

Implementation:

// Check network status
if (isOnline) {
    // Direct submission to server
    tournamentApi.submitRound(round)
} else {
    // Cache for later sync
    scoreCacheDao.insert(TournamentScoreCacheEntity(
        tournamentId = tournament.id,
        participantId = userId,
        roundNumber = roundNumber,
        scores = arrowScores,
        syncStatus = SyncStatus.PENDING
    ))
}

Advanced Features

Elimination Rounds

Concept: Top participants advance to next round.

Implementation:

// After round 1 completes
val allParticipants = participantDao.getLeaderboard(tournamentId)
val advancingCount = (allParticipants.size * 0.5).toInt()  // Top 50%
val advancing = allParticipants.take(advancingCount)
 
// Mark others as completed/eliminated
allParticipants.drop(advancingCount).forEach { participant ->
    participantDao.updateStatus(participant.id, ParticipantStatus.COMPLETED)
}

Tiebreaking

Common Rules:

  1. X-Count - Most X-ring hits
  2. Last End - Highest score in final end
  3. Closest Arrow - Distance to center of best arrow

Example Query:

// Tiebreaking by X-count
@Query("""
    SELECT p.*, COUNT(a.isXRing) as xCount
    FROM TournamentParticipant p
    JOIN Round r ON r.tournamentId = p.tournamentId AND r.participantId = p.userId
    JOIN ArrowScore a ON a.roundId = r.id
    WHERE p.tournamentId = :tournamentId
    AND p.totalScore = :tiedScore
    GROUP BY p.id
    ORDER BY xCount DESC
""")
suspend fun breakTie(tournamentId: String, tiedScore: Int): List<ParticipantWithXCount>

Scoring:

  • Round - Tournament rounds link via tournamentId
  • ArrowScore - Individual scores in tournament rounds

Equipment:

  • BowSetup - Participants use their equipment

Developer Guidelines

Best Practices

1. Always Check Tournament Status

// Good: Verify status before actions
if (tournament.status == TournamentStatus.OPEN) {
    allowRegistration()
} else {
    showError("Tournament registration closed")
}
 
// Bad: Assume tournament is open
createParticipant()  // May fail if tournament already started

2. Handle Offline Gracefully

// Good: Cache offline, sync later
if (isOnline) {
    submitScoresDirectly()
} else {
    cacheScoresForLaterSync()
}
 
// Bad: Fail on offline
submitScoresDirectly()  // Throws exception if offline

3. Recalculate Rankings After Score Changes

// Good: Update rankings
submitRound(round)
updateTournamentRankings(tournamentId)
 
// Bad: Stale rankings
submitRound(round)
// Rankings not updated, leaderboard shows old data

4. Enforce Deadlines

// Good: Check deadlines before submission
if (tournament.roundDeadlines[roundNumber]?.let { it > Clock.System.now() } == true) {
    allowSubmission()
} else {
    showError("Round deadline passed")
}

Common Queries

Get active tournaments:

@Query("""
    SELECT * FROM TournamentEntity
    WHERE status IN ('OPEN', 'IN_PROGRESS')
    ORDER BY startTime ASC
""")
suspend fun getActiveTournaments(): List<TournamentEntity>

Get user’s tournaments:

@Query("""
    SELECT t.* FROM TournamentEntity t
    JOIN TournamentParticipant p ON p.tournamentId = t.id
    WHERE p.userId = :userId
    ORDER BY t.createdAt DESC
""")
suspend fun getUserTournaments(userId: String): List<TournamentEntity>

Get tournament with participants:

data class TournamentWithParticipants(
    val tournament: TournamentEntity,
    val participants: List<TournamentParticipantEntity>
)
 
@Transaction
@Query("SELECT * FROM TournamentEntity WHERE id = :tournamentId")
suspend fun getTournamentWithParticipants(tournamentId: String): TournamentWithParticipants

Database:

Other Data Models:

User Guides:


Quick Reference

EntityFieldsPurposeCascade
TournamentEntity36Tournament metadata and configurationParent → deletes participants
TournamentParticipantEntity10Participant progress and rankingChild of tournament
TournamentScoreCache7Offline score caching for syncStandalone (no FK)

Tournament Status Flow: OPEN → IN_PROGRESS → COMPLETED (or CANCELLED)

Participant Status: ACTIVE, WITHDRAWN, DISQUALIFIED, COMPLETED

Sync Status: PENDING, SYNCED, FAILED

Tournament Types:

  • Local (isLocal = true): Offline, no sync
  • Remote (isLocal = false): Online, real-time sync
  • Hybrid: Remote with offline caching

Next: Analytics Data Models - Performance metrics and caching entities

0 items under this folder.