# Voting and Popularity

A narrative guide to how the Jackbox Game Picker handles community voting and game popularity. This system lets viewers and stream chat influence which games rise to the top—without directly controlling the random picker.

---

## 1. How Popularity Works

Every game has a **popularity score** stored in the database:

```
popularity_score = upvotes - downvotes
```

The score is computed from `upvotes` and `downvotes` and persisted per game. As votes accumulate across sessions, the score reflects community sentiment over time.

**Important:** Popularity is used for **rankings** (e.g., "top rated games" in stats) but **does not directly affect picker weights**. The random picker uses favor bias, not popularity, when selecting games.

---

## 2. Favor Bias vs Popularity

Two separate systems govern how games are treated:

| Aspect | **Favor Bias** | **Popularity** |
|--------|----------------|----------------|
| Who controls it | Admin (via API) | Community (via votes) |
| Values | `-1` (disfavor), `0` (neutral), `1` (favor) | `upvotes - downvotes` (unbounded) |
| Affects picker? | Yes — directly changes weights | No |
| Purpose | Manual curation; push/penalize specific games | Community sentiment; rankings |

**Favor bias** affects picker probability directly. Setting `favor_bias` to `1` on a game boosts its weight; `-1` reduces it. See [Games favor endpoint](../endpoints/games.md#patch-apigamesidfavor) and [Picker weighted selection](../endpoints/picker.md#weighted-selection).

**Popularity** is driven entirely by viewer votes. It surfaces in stats (e.g., `topRatedGames`) and session game lists, but the picker does not read it. These systems are independent.

---

## 3. Two Voting Mechanisms

The API supports two ways to record votes: batch chat import (after the fact) and live votes (real-time from bots).

### Chat Import (Batch, After-the-Fact)

Collect Twitch or YouTube chat logs containing `thisgame++` (upvote) and `thisgame--` (downvote), then submit them in bulk.

**Flow:**
1. Export chat logs with `username`, `message`, and `timestamp` for each message.
2. Filter or pass messages; the API parses `thisgame++` and `thisgame--` from the `message` field.
3. POST to `POST /api/sessions/{id}/chat-import` with a `chatData` array of `{ username, message, timestamp }`.
4. The API matches each vote’s timestamp to the game that was playing at that time (using `played_at` intervals).
5. Votes are deduplicated by SHA-256 hash of `username:message:timestamp`.
6. Response includes `votesByGame` breakdown and `debug` info (e.g., session timeline, vote matches).

See [Sessions chat-import endpoint](../endpoints/sessions.md#post-apisessionsidchat-import).

### Live Votes (Real-Time, from Bots)

A bot sends individual votes during the stream. Each vote is processed immediately.

**Flow:**
1. Bot detects `thisgame++` or `thisgame--` (or equivalent) in chat.
2. Bot sends `POST /api/votes/live` with `{ username, vote, timestamp }`.
3. `vote` must be `"up"` or `"down"`.
4. `timestamp` must be ISO 8601 (e.g., `2026-03-15T20:30:00Z`).
5. The API finds the active session and matches the vote timestamp to the game playing at that time.
6. **Deduplication:** Votes from the same username within 1 second are rejected with `409 Conflict`.

See [Votes live endpoint](../endpoints/votes.md#post-apivoteslive).

**Real-time tracking:** Live votes also broadcast a `vote.received` WebSocket event to all clients subscribed to the active session. This enables stream overlays and bots to react to votes in real-time without polling. See [WebSocket vote.received](../websocket.md#votereceived).

---

## 3b. Querying Vote Data

Two endpoints expose vote data for reading:

- **`GET /api/sessions/{id}/votes`** — Per-game vote breakdown for a session. Returns aggregated `upvotes`, `downvotes`, `net_score`, and `total_votes` per game. See [Sessions votes endpoint](../endpoints/sessions.md#get-apisessionsidvotes).
- **`GET /api/votes`** — Paginated global vote history with filtering by `session_id`, `game_id`, `username`, and `vote_type`. Returns individual vote records. See [Votes list endpoint](../endpoints/votes.md#get-apivotes).

---

## 4. Timestamp Matching Explained

Games in a session have a `played_at` timestamp. A vote’s timestamp determines which game it belongs to.

**Rule:** A vote belongs to the game whose `played_at` is the **most recent one before** the vote timestamp.

Example session timeline:

- Game A: `played_at` 20:00
- Game B: `played_at` 20:15
- Game C: `played_at` 20:30

- Vote at 20:10 → Game A (last `played_at` before 20:10)
- Vote at 20:20 → Game B
- Vote at 20:45 → Game C (last game in session; captures all votes after it started)

The **last game** in the session captures all votes that occur after its `played_at`.

---

## 5. How Stats Reflect Popularity

`GET /api/stats` returns aggregate statistics, including:

- **mostPlayedGames** — top 10 by `play_count` (games with `play_count` > 0).
- **topRatedGames** — top 10 by `popularity_score` (games with `popularity_score` > 0).

Both are limited to the top 10 and exclude games with score/count ≤ 0. See [Stats endpoint](../endpoints/stats.md).

---

## 6. Example Requests

### Chat Import

Import a batch of chat messages for session `5`:

```bash
curl -X POST "http://localhost:5000/api/sessions/5/chat-import" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "chatData": [
      {
        "username": "viewer1",
        "message": "thisgame++",
        "timestamp": "2026-03-15T20:30:00Z"
      },
      {
        "username": "viewer2",
        "message": "thisgame--",
        "timestamp": "2026-03-15T20:31:00Z"
      },
      {
        "username": "viewer3",
        "message": "thisgame++",
        "timestamp": "2026-03-15T20:32:00Z"
      }
    ]
  }'
```

**Sample response (200 OK):**

```json
{
  "message": "Chat log imported and processed successfully",
  "messagesImported": 3,
  "duplicatesSkipped": 0,
  "votesProcessed": 3,
  "votesByGame": {
    "42": {
      "title": "Quiplash 3",
      "upvotes": 2,
      "downvotes": 1
    }
  },
  "debug": {
    "sessionGamesTimeline": [
      {
        "title": "Quiplash 3",
        "played_at": "2026-03-15T20:00:00.000Z",
        "played_at_ms": 1742068800000
      }
    ],
    "voteMatches": []
  }
}
```

### Live Vote

Submit a single live vote (requires active session):

```bash
curl -X POST "http://localhost:5000/api/votes/live" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "viewer123",
    "vote": "up",
    "timestamp": "2026-03-15T20:30:00Z"
  }'
```

**Sample response (200 OK):**

```json
{
  "success": true,
  "message": "Vote recorded successfully",
  "session": { "id": 3, "games_played": 5 },
  "game": {
    "id": 42,
    "title": "Quiplash 3",
    "upvotes": 11,
    "downvotes": 2,
    "popularity_score": 9
  },
  "vote": {
    "username": "viewer123",
    "type": "up",
    "timestamp": "2026-03-15T20:30:00Z"
  }
}
```

---

## Related Documentation

- [Sessions endpoints](../endpoints/sessions.md) — chat import, session games, `played_at`
- [Votes endpoints](../endpoints/votes.md) — live votes, deduplication, errors
- [Stats endpoints](../endpoints/stats.md) — `mostPlayedGames`, `topRatedGames`
- [Picker endpoints](../endpoints/picker.md) — weighted selection, favor bias (no popularity)
- [Games endpoints](../endpoints/games.md) — favor bias per game and pack
