Binary Match Logs

Comprehensive match logging system using compact binary format for efficient storage, transfer, and replay.

Overview

binary-v2 encodes coordinates as little-endian u16 (low * (maxPip + 1) + high), covering Warp 9 / 12 / 15 / 18.

The binary match log system achieves 50-500x compression over JSON, enabling:

  • Local storage of thousands of matches in IndexedDB
  • Cross-device sync via Firestore
  • Shareable match links (1-2KB for typical game)
  • Full match replay with round-boundary checkpoints

Architecture

┌─────────────────────────────────────────────────────────┐
│                    Match Logging                        │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  Game Play → BinaryMatchLog → Binary Export            │
│                     ↓                                   │
│              ┌──────┴──────┐                           │
│              ↓             ↓                            │
│         IndexedDB      Firestore                        │
│         (local)        (cloud)                          │
│              ↓             ↓                            │
│         Replay      Cross-device                        │
│                        Sync                             │
└─────────────────────────────────────────────────────────┘

Components

1. Binary Action Encoding (libs/engine/src/lib/serialization/)

Encodes GameActions to 2–6+ bytes each:

import { encodeActions, decodeActions } from 'warp12-engine';

const ctx = { playerIds: ['p0', 'p1', 'p2', 'p3'], maxPip: 12 };
const actions: GameAction[] = [...]; // 200 actions

// Encode to binary (~600 bytes for typical match)
const binary = encodeActions(actions, ctx);

// Decode back to full objects
const decoded = decodeActions(binary, ctx);

Supported Actions:

  • ✅ All 15 core action types
  • ✅ All 9 continuum flash effects
  • ✅ All module actions (Gamma sensor, Delta spool, Epsilon drafting, Zeta impulse)
  • ✅ House rules (shield control)
  • ✅ Classic root line-ends (compact route byte) and nested ALL_DOUBLES paths (CHART_LINE_PATH = 0x1a: end + pathLen + path bytes, max depth 16)

2. State Snapshot Encoding

Encodes RoundState/GameState to ~300 bytes:

import { encodeRoundState, decodeRoundState } from 'warp12-engine';

const ctx = { maxPip: 12 };
const round: RoundState = {...};

// Encode (~200-400 bytes)
const binary = encodeRoundState(round, ctx);

// Decode back
const decoded = decodeRoundState(binary, { 
  maxPip: 12, 
  playerIds: ['p0', 'p1', 'p2', 'p3'] 
});

3. Match Log Accumulator (apps/Warp12/src/game/match-log-binary.ts)

Accumulates actions + optional state snapshots:

import { BinaryMatchLog } from './game/match-log-binary.js';

const log = new BinaryMatchLog({
  gameId: 'match-123',
  playerIds: ['p0', 'p1', 'p2', 'p3'],
  maxPip: 12,
  captureSnapshots: true, // Optional round checkpoints
});

// During game
log.recordAction(action);
log.recordRoundEnd(roundState); // Captures checkpoint

// Export
const exported = log.export();
console.log(`${exported.actions.byteSize} bytes`);

4. IndexedDB Storage (apps/Warp12/src/storage/match-log-db.ts)

Local storage for thousands of matches:

import { openMatchLogDB, storeMatch, listRecentMatches } from './storage/match-log-db.js';

const db = await openMatchLogDB();

// Store
await storeMatch(db, matchLog);

// Retrieve recent
const recent = await listRecentMatches(db, 50);

// Query by player
const myMatches = await findMatchesByPlayer(db, 'player-123');

5. Base64 Export/Import (apps/Warp12/src/game/match-log-transfer.ts)

Shareable match logs as compact strings:

import { 
  exportMatchToBase64Url, 
  importMatchFromBase64,
  createShareableLink 
} from './game/match-log-transfer.js';

// Export for sharing
const base64 = exportMatchToBase64Url(matchLog);
// Copy/paste or generate link
const link = createShareableLink(matchLog);
// https://warp.iwgf.org?match=eyJnYW1l...

// Import
const imported = importMatchFromBase64(base64);
const fromUrl = extractMatchFromUrl(window.location.href);

6. Cloud Sync (apps/Warp12/src/storage/match-sync.ts)

Cross-device match history:

import { createMatchSyncService } from './storage/match-sync.js';

const sync = await createMatchSyncService(firestore, uid);

// Store locally and sync to cloud
await sync.storeAndSync(matchLog);

// Pull matches from other devices
const pulled = await sync.pullFromCloud();
console.log(`Downloaded ${pulled} new matches`);

// Sync all pending
const stats = await sync.syncPending();
console.log(`Uploaded: ${stats.uploaded}, Errors: ${stats.errors}`);

Compression Ratios

ComponentJSON SizeBinary SizeRatio
Single CHART action~120 bytes4 bytes30x
200-action match~30KB~600 bytes50x
Round state snapshot~20KB~300 bytes66x
Full match with 10 snapshots~250KB~3.5KB70x

Export Formats

Standard Export (JSON + Binary)

{
  "gameId": "match-123",
  "actions": {
    "format": "binary-v2",
    "encoding": "base64",
    "data": "AQIDBAUG...",
    "actionCount": 200,
    "byteSize": 612,
    "playerIds": ["p0", "p1", "p2", "p3"],
    "maxPip": 12
  },
  "snapshots": [
    {
      "round": 1,
      "data": "AgMEBQYH...",
      "byteSize": 287,
      "timestamp": 1700000001000
    }
  ],
  "exportedAt": 1700000000000
}

URL-Safe Base64

eyJnYW1lSWQiOiJtYXRjaC0xMjMiLCJhY3Rpb25zIjp7ImZvcm1hdCI6ImJpbmFyeS12MSIsImVuY29kaW5nIjoiYmFzZTY0IiwiZGF0YSI6IkFRSURCQVVHIiwiYWN0aW9uQ291bnQiOjIwMCwiYnl0ZVNpemUiOjYxMiwicGxheWVySWRzIjpbInAwIiwicDEiLCJwMiIsInAzIl0sIm1heFBpcCI6MTJ9LCJleHBvcnRlZEF0IjoxNzAwMDAwMDAwMDAwfQ

Firestore Schema

userMatchLogs/{uid}/matches/{matchId}
  - gameId: string
  - actions: map (binary format)
  - snapshots: array (optional)
  - exportedAt: timestamp
  - uploadedAt: timestamp

Usage Examples

Basic Match Logging

// Setup
const log = new BinaryMatchLog({
  gameId: game.id,
  playerIds: game.captains.map(c => c.id),
  maxPip: game.maxPip,
});

// During game
applyAction(game, action);
log.recordAction(action);

// At round end
if (game.round?.phase === 'ended') {
  log.recordRoundEnd(game.round);
}

// Export when game complete
const exported = log.export();
await storeMatch(db, exported);

Match Replay

// Load from IndexedDB
const match = await getMatchesByGameId(db, 'match-123')[0];

// Decode actions
const ctx = {
  playerIds: match.actions.playerIds,
  maxPip: match.actions.maxPip,
};
const binary = atob(match.actions.data);
const uint8 = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
  uint8[i] = binary.charCodeAt(i);
}
const actions = decodeActions(uint8, ctx);

// Replay game
let state = createInitialGameState(...);
for (const action of actions) {
  state = applyAction(state, action).state;
}

Cross-Device Sync

// On device A: Store and sync
const sync = await createMatchSyncService(firestore, user.uid);
await sync.storeAndSync(matchLog);

// On device B: Pull new matches
const pulled = await sync.pullFromCloud();
console.log(`Synced ${pulled} matches from other devices`);

// Check sync status
const status = await sync.getSyncStatus();
console.log(`${status.synced}/${status.total} matches synced`);

Share via URL

// Create shareable link
const link = createShareableLink(matchLog);
// https://warp.iwgf.org?match=eyJnYW1l...

// Copy to clipboard
navigator.clipboard.writeText(link);

// Recipient opens link
const match = extractMatchFromUrl();
if (match) {
  // Store locally and replay
  await storeMatch(db, match);
}

Testing

Run comprehensive tests:

# Action round-trip (all 15 types, all 9 flash effects)
yarn vitest run libs/engine/src/lib/serialization/action-roundtrip.spec.ts

# State encoding
yarn vitest run libs/engine/src/lib/serialization/encode-state.spec.ts
yarn vitest run libs/engine/src/lib/serialization/decode-state.spec.ts

# Match log accumulator
yarn vitest run apps/Warp12/src/game/match-log-binary.spec.ts

# Transfer/export
yarn vitest run apps/Warp12/src/game/match-log-transfer.spec.ts

Security

Firestore Rules

User match logs are private:

  • Users can only read/write their own logs
  • Anonymous matches stored with auth UID
  • See firestore-match-logs.rules

Data Privacy

  • Match logs contain no personal data (player IDs are local)
  • Binary format is deterministic (no secrets embedded)
  • Base64 URLs are safe to share publicly
  • Cloud sync requires authentication

Performance

Storage:

  • 1000 matches ~= 600KB - 3MB (with snapshots)
  • IndexedDB handles 10K+ matches easily

Transfer:

  • Typical match: 1-2KB
  • Fast sync over cellular
  • CloudFlare CDN for Firebase Hosting

Replay:

  • Decode 200 actions: <1ms
  • Full match replay: <10ms
  • State snapshot restore: <1ms

Future Enhancements

  • Compression (gzip) for cloud storage
  • Incremental sync (only new rounds)
  • Selective sync (by date range, player, objective)
  • Match analysis metadata (win rates, average scores)
  • Batch export for AI training
  • Tournament bracket tracking

Warp 12 — a Double-Twelve domino variant for the NX Epoch.