Skip to content

Latest commit

 

History

History
183 lines (130 loc) · 6.65 KB

spec.md

File metadata and controls

183 lines (130 loc) · 6.65 KB

Pose Battle — game spec

App built to run at the KNI / URz open day. Hardware: Raspberry Pi 5 + Hailo-10H + camera + HDMI monitor. Uses YOLO pose detection.

Concept

A game for teams of 3. Teams compete at reproducing displayed body poses. Each team plays on its own, sequentially — one team in front of the camera at a time. After playing, the team lands on a leaderboard visible to everyone.

Every team plays the same poses — fair comparison, the leaderboard makes sense.

Flow of a single team session

  1. Team name entry — short, for the leaderboard
  2. Calibration — each player stands in their zone (the screen is split into 3 vertical lanes: left/centre/right). The system waits until the lanes are stably filled. The required count is configurable in pose_battle.py: MIN_PLAYERS = 3 (event mode — all three zones must be stably occupied) or MIN_PLAYERS = 2 (test mode — starts with 2, a late third player may join within a T_CALIB_GRACE window). It currently ships set to 2.
  3. Rounds — one round per pose, each round follows the same pattern
  4. Summary — final score, leaderboard position, write to storage

After the summary it returns to the leaderboard and waits for the next team.

Flow of a single round

  1. Intro (2s) — "Round X/N", pose name, difficulty
  2. Memorize (2.5s) — the target pose shown large in the centre of the screen as a skeleton, with a countdown
  3. Pose window (3s) — the preview disappears (a small one stays in the corner), players try to reproduce it, the system collects live scores
  4. Reveal (2.5s) — results: per-player score, team score, bonuses, multiplier

~10 s per round total.

Pose pool (fixed, the same for every team)

The game loads every pose file from poses/, sorted by name, and plays all of them — one pose per round. It started as a pool of 5 and was later extended to 10, so a session is now 10 rounds. The current pool, with difficulty, tolerance and multiplier for each pose, is documented in ../poses/README.md.

Design intent for the difficulty tiers:

Difficulty Tolerance Multiplier Character
Easy wide ×1.0 symmetric / simple (e.g. T-pose)
Medium medium ×1.3 full body (e.g. warrior from yoga)
Hard tight ×1.6 asymmetric / complex (e.g. sprinter, star)

Each pose is defined in JSON as a set of target joint angles with per-joint weights (arms/elbows weigh more than fingers/neck). Poses load from files — easy to edit without touching the code.

A separate tool (script) is needed to record poses: stand in front of the camera, press space, it averages 30 frames and saves the target angles to a JSON under the given name.

Player identification

Lane mode — the screen is split into 3 vertical zones. Each player stands in their own. The system assigns detections to players by the X position of the mid-hip:

  • left lane → Player 1
  • centre lane → Player 2
  • right lane → Player 3

If a player leaves their lane / 2 people are in one lane / a lane is empty → score 0 for the affected players in those frames + a visual warning on screen.

Scoring — how points are computed

Per player, per frame

For each joint defined in the target pose, the current angle is computed from 3 keypoints (e.g. elbow = shoulder-elbow-wrist angle) and compared to the target.

joint_score = max(0, 1 - |current - target| / tolerance)
player_score = weighted_average(joint_scores)  # 0.0 - 1.0

Joints with keypoint confidence < 0.3 are skipped (player partially occluded). If fewer than 4 joints from the target list are visible → player_score = 0 (anti-cheese).

Per player, per round

From the 3-second pose window the system takes the maximum score reached in any frame + the timestamp of the moment the player first crossed 0.8 (used to compute sync).

This forces players to hold the pose for at least a moment, but does not penalise small jitter.

Team score, per round

team_avg = mean(p1, p2, p3)
cohesion = how close the three players' scores are to each other
base_score = team_avg × cohesion_bonus × 100
round_score = base_score × difficulty_multiplier

Cohesion bonus (up to +30%): rewards a team where everyone is equally good. Three players at 70% > one at 100% + two at 55%. Forces team play.

Bonuses added to round_score

  • Perfect bonus (+30 pts) — if team_avg ≥ 95%
  • Sync bonus (+15 pts) — if all 3 players crossed the 80% threshold within a ≤300 ms window (synchronised execution, not one after another)
  • Streak bonus (+10 pts × consecutive round) — for consecutive rounds with team_avg ≥ 80%, reset when it drops below

Team final score

Sum of round_score + bonuses across all rounds. Goes to the leaderboard.

Visual feedback during the game

Skeleton overlay on each player's live feed. Per-joint colours updated live:

  • green — perfect match (diff < 40% of tolerance)
  • yellow — close (diff < tolerance)
  • red — off (diff > tolerance)
  • grey — joint invisible (low confidence)

A live score % and a progress bar under each player. Players immediately see what to fix.

Leaderboard

Persisted across sessions and app restarts. Shows the top 10 teams on the start screen. After a session, an animation shows the new team's position in the ranking.

Data persistence

Everything is saved locally so that results can be analysed in Pandas/Excel after the event. For each team session it saves:

  • team name, timestamp, total score
  • per round: pose, score per player, team avg, bonuses, round total
  • per round per player: every joint, target angle, achieved angle, joint score (for deep analysis like "which team had the worst elbows")

One-button CSV export.

Controls and edge cases

  • A key to start a new session (from the idle screen)
  • A key to abort a session (back to idle without saving)
  • A key to export CSV
  • A key to toggle fullscreen

The app must not crash on:

  • 0 people in frame
  • 4+ people in frame
  • a player briefly occluded
  • camera disconnect
  • team names with Polish characters / emoji / spaces

Performance requirements

  • A steady 30 FPS during the pose window (pose detection must not block UI rendering)
  • The app runs for the whole event (several hours, dozens of teams) without a restart, memory leaks or degradation

Event assumptions

  • Set up once, then self-service — a moderator presses "start", the rest runs on its own
  • Public-facing screen (the audience sees the game live + the leaderboard)
  • After the event I run the analysis from the CSV: mean scores per pose, hardest joints, who had the best sync, etc.