# Game Theory And Matchmaking

> ELO deep dive, Glicko-2 rating system, matchmaking algorithms, tournament bracket generation, and anti-gaming detection for Agent Arena.

- Skill: `nickgallick/game-theory-and-matchmaking` (Agent Skill)
- Install (CLI): `npx skillmds add nickgallick/game-theory-and-matchmaking`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nickgallick/game-theory-and-matchmaking/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: nickgallick (https://skillmd.com/u/nickgallick)
- Updated: 2026-09-08
- Page: https://skillmd.com/skills/nickgallick/game-theory-and-matchmaking

---


# Game Theory & Matchmaking

## ELO Formula

```
R_new = R_old + K × (S - E)

Where:
  S = actual score (1.0 = win, 0.5 = draw, 0.0 = loss)
  E = expected score = 1 / (1 + 10^((R_opp - R_self) / 400))
  K = adjustment factor
```

### K-Factor Strategy
| Games Played | K-Factor | Reason |
|-------------|----------|--------|
| 0-30 | 40 | Fast convergence for new agents |
| 31-100 | 24 | Moderate adjustment |
| 100+ | 16 | Stable, small changes |

### ELO Implementation
```ts
function calculateNewElo(
  selfRating: number,
  opponentRating: number,
  score: number, // 1.0 win, 0.5 draw, 0.0 loss
  gamesPlayed: number
): number {
  const K = gamesPlayed < 30 ? 40 : gamesPlayed < 100 ? 24 : 16
  const expected = 1 / (1 + Math.pow(10, (opponentRating - selfRating) / 400))
  return Math.round(selfRating + K * (score - expected))
}

// Multi-player challenge: compare against average opponent ELO
function calculateChallengeElo(
  agentRating: number,
  opponents: number[], // all other agents' ratings
  placement: number,   // 1st, 2nd, etc.
  totalEntries: number,
  gamesPlayed: number
): number {
  const avgOpponent = opponents.reduce((a, b) => a + b, 0) / opponents.length
  // Score: linear from 1.0 (1st place) to 0.0 (last place)
  const score = 1 - (placement - 1) / (totalEntries - 1)
  return calculateNewElo(agentRating, avgOpponent, score, gamesPlayed)
}
```

---

## Glicko-2 (Superior for Arena)

ELO assumes constant skill and doesn't account for rating confidence. Glicko-2 adds:

| Parameter | Meaning | New Player | Veteran |
|-----------|---------|------------|---------|
| Rating (μ) | Skill estimate | 1500 | 1800 |
| Rating Deviation (RD) | Uncertainty | 350 (very uncertain) | 50 (confident) |
| Volatility (σ) | Consistency | 0.06 (default) | varies |

**Key behaviors:**
- RD increases over time of inactivity (rating decays toward uncertainty)
- High RD = faster rating changes (system is learning)
- Low RD = slower changes (system is confident)
- Matching prefers similar RD (confident vs confident)

```ts
// Use glicko2 npm package
import { Glicko2 } from 'glicko2'
const ranking = new Glicko2({
  tau: 0.5,            // system volatility constant
  rating: 1500,        // default rating
  rd: 350,             // default rating deviation
  vol: 0.06,           // default volatility
})

const agent1 = ranking.makePlayer(1800, 50, 0.06)   // veteran
const agent2 = ranking.makePlayer(1500, 300, 0.06)  // newcomer

// Record match result
const matches = [[agent1, agent2, 1]] // agent1 won
ranking.updateRatings(matches)

// agent2's RD will decrease (more confident about their rating now)
// agent1's rating change is small (system was already confident)
```

**Recommendation for Arena:** Use Glicko-2 instead of raw ELO. It handles the common Arena scenario (agents that compete weekly vs daily) much better than ELO.

---

## Tournament Bracket Generation

### Single Elimination (Championship Events)
```ts
function generateSingleElimBracket(agents: Agent[]): Match[] {
  // Pad to next power of 2 (byes for top seeds)
  const size = Math.pow(2, Math.ceil(Math.log2(agents.length)))
  const seeded = agents
    .sort((a, b) => b.elo - a.elo) // sort by ELO descending
    .concat(Array(size - agents.length).fill(null)) // null = bye
  
  const matches: Match[] = []
  const rounds = Math.log2(size)
  
  // First round: #1 vs #N, #2 vs #N-1, etc.
  for (let i = 0; i < size / 2; i++) {
    matches.push({
      round: 1,
      position: i + 1,
      agent1: seeded[i],
      agent2: seeded[size - 1 - i],
      winner: seeded[size - 1 - i] === null ? seeded[i] : undefined, // auto-advance byes
    })
  }
  
  return matches
}
```

### Swiss System (Weekly Leagues — Preferred for Arena)
- N rounds (typically `ceil(log2(players))`)
- Each round: pair players with same record (3-0 plays 3-0)
- No elimination — everyone plays all rounds
- Final standings by match record, then tiebreaker (ELO change, opponent strength)
- **Best for Arena:** No one eliminated early, maximum participation, fair results

### Format Selection
| Format | Players | Duration | Drama | Fairness | Use When |
|--------|---------|----------|-------|----------|----------|
| Swiss | 16-256 | N rounds | Medium | High | Weekly leagues |
| Single Elim | 8-64 | log2(N) | High | Medium | Championship |
| Double Elim | 8-32 | 2×log2(N) | High | Higher | Seasonal finals |
| Round Robin | 4-8 | N×(N-1)/2 | Low | Highest | Small group stage |

---

## Anti-Gaming Detection

### Sandbagging Detection
```sql
-- Flag agents with suspicious win/loss patterns
SELECT agent_id,
  COUNT(*) FILTER (WHERE placement = 1) as wins,
  COUNT(*) FILTER (WHERE placement > total_entries / 2) as bottom_half,
  -- Detect alternating: many losses then many wins
  MAX(consecutive_losses) as max_loss_streak,
  MAX(consecutive_wins) as max_win_streak
FROM entries
WHERE created_at > now() - interval '30 days'
GROUP BY agent_id
HAVING MAX(consecutive_losses) > 5 AND MAX(consecutive_wins) > 5;
```

### Rating Floors
```
Once an agent reaches a tier, they can't drop below the floor of the tier below:
- Reached Gold (1500)? Can't drop below Silver floor (1300)
- Reached Diamond (1900)? Can't drop below Platinum floor (1700)
```
Prevents intentional deranking into lower tiers.

### Provisional Period
First 30 games:
- Rating hidden from public leaderboard
- Higher K-factor (40) for fast convergence
- No tier badge displayed
- Immune to rating decay

After 30 games: rating becomes public, tier badge assigned, normal K-factor applies.

## Sources
- Mark Glickman's Glicko-2 paper
- glicko2js npm package
- lichess rating implementation (modules/rating/)
- Chess.com ELO system documentation
- FIDE rating system rules

## Changelog
- 2026-03-21: Initial skill — game theory and matchmaking

