Per plan §3.8, maps should be generated offline and stored in the map
library, not generated on-the-fly during matches. This commit adds
support for loading pre-generated maps from the database.
Changes:
- Add PreGeneratedMap type and WithMap option to MatchRunner
- Add loadPreGeneratedMap() to parse map JSON (walls, cores)
- Update worker to pass loaded map data to MatchRunner via WithMap
- Fallback to on-the-fly generation if map data is invalid
- Update acb-mapgen spawn radius to 25% for 2-player (aligns with match.go)
- Update test to verify cores are outside final zone radius
This enables the map library infrastructure (maps/, acb-mapgen, index
builder) to be used in production matches instead of being ignored.
Closes: bf-5m29
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The worker was hardcoding AttackRadius2=5 in executeMatch, but
engine.ConfigForPlayers sets AttackRadius2=12 for both 2-player and
3+ matches. This mismatch meant matches ran with the old attack radius
instead of the improved value that supports better combat density.
Now uses ConfigForPlayers which provides:
- AttackRadius2: 12 (3.5 tiles) for all player counts
- Proper zone parameters scaled by player count
- Correct max turns scaling
Grid dimensions are overridden from the pre-generated map, and
SeasonID/RulesVersion are preserved from the match.
Closes: bf-576s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Fix computeCombatTurns to count EventCombatDeath events instead of
EventBotDied with reason="combat" (which was never emitted, causing
CombatTurns to always be 0)
- Add CombatDeaths field to MapEngagementScore to track focus-fire kills
- Update engagement formula to weight combat deaths at 3.0 (same as
win_prob_crossings) to bias map evolution toward combat-dense maps
- Add countCombatDeaths helper function to count EventCombatDeath events
- Update log output to include combat_deaths metric
This implements bf-4nxs: the combat-density metric is now measured and
weighted in map engagement, which gates map curation/selection. Maps
with zero combat will have low engagement scores and be filtered out.
Closes: bf-4nxs
Update the map engagement scoring formula to match plan §14.6:
- score = win_prob_crossings * 3.0 + critical_moments * 2.0 +
resource_contest_turns * 1.5 + survival_turns * 0.5
New metrics computed from replay data:
- resource_contest_turns: turns where energy is contested by multiple players
- survival_turns: turns where all players have at least one bot alive
The old formula used map_coverage_pct, closeness, and turn_pct which
did not match the specification.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The rating recovery CLI mode (-mode=recalc-ratings) was using
glicko2Tau (0.5) instead of glicko2DefaultSigma (0.06) for the
default sigma value when resetting ratings. This caused the reset
sigma to be ~8x higher than the schema-defined default.
Added glicko2DefaultSigma constant (0.06) and updated ResetAllRatings
and recalcRatings to use it correctly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements the rating recovery procedure specified in plan §12.3.
Running 'go run ./cmd/acb-worker -mode=recalc-ratings' will:
1. Reset all bot ratings to Glicko-2 defaults (mu=1500, phi=350, sigma=0.06)
2. Fetch all completed matches from the database in chronological order
3. Replay each match to recompute Glicko-2 ratings from scratch
4. Update the bots table with the recalculated ratings
This is needed for disaster recovery when ratings are corrupted or lost.
Database functions added:
- ResetAllRatings: resets all bot ratings to defaults
- GetAllCompletedMatches: fetches completed matches chronologically with participants
- UpdateAllRatings: bulk updates all bot ratings in a single transaction
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add engine.CalculateMapEngagement() to compute map engagement scores from replay data (win_prob_crossings, critical_moments, map_coverage_pct, closeness, turn_pct)
- Add DBClient.UpdateMapEngagement() to update map engagement using rolling average
- Worker now calculates and writes map engagement scores after each match
- Add test to verify win_prob array is non-empty in produced replays
This implements the win probability Monte Carlo array storage in replay JSON
feature. The engine already called ComputeWinProbability() in MatchRunner.Run(),
so this commit adds the missing map engagement tracking.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds combat_turns metric (distinct turns where ≥1 bot died from enemy
focus-fire, excluding self-collisions). Worker computes it after each
match; index builder sorts matches/index.json and the new most-combat
playlist descending by it, and bumps interest score for combat-heavy
matches so they surface in highlights.
Also switches homepage featured replay default view from influence to
standard so the actual bot-on-bot combat is visible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
map_json generated by acb-map-evolver lacks a 'spawns' key; scanning
map_json->>'spawns' into a non-nullable string causes "converting NULL
to string is unsupported". Use COALESCE for walls/spawns/cores.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The encryption key stored in OpenBao/K8s secrets is base64-encoded but
the API and worker crypto functions expected hex. Add parseAESKey() that
accepts both formats (tries hex first, falls back to base64).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two root causes prevented bots from making any moves:
1. SignRequest signing string included timestamp ({match_id}.{turn}.{timestamp}.{hash})
but all bots implement verifySignature without timestamp ({match_id}.{turn}.{hash}).
Fixed by dropping timestamp from the signing string; X-ACB-Timestamp header is still
sent for clock-skew checks but not in the HMAC.
2. The API stores bot secrets AES-GCM encrypted (184 hex chars) in the DB. The worker
was passing the ciphertext directly as the HMAC key, while bots use their plaintext
k8s secret (64 hex chars). Fixed by decrypting in the worker using ACB_ENCRYPTION_KEY.
Also tightens the home page winner filter to exclude winner_id="0" stalemates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AWS SDK Go v2 s3 v1.100.0 defaults to RequestChecksumCalculationWhenSupported,
which causes PutObject to send STREAMING-UNSIGNED-PAYLOAD-TRAILER — a chunked
transfer mode R2 doesn't support. Setting WhenRequired makes the SDK send a
standard signed payload instead, resolving the 403 SignatureDoesNotMatch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds R2 (Cloudflare) as a direct upload target alongside B2 (cold archive).
When ACB_R2_* credentials are configured, the worker uploads replays and
thumbnails to R2 immediately after each match, bypassing the index-builder's
B2→R2 promotion cycle.
This is necessary because ARMOR's B2 app key is write-only; reads via the
direct S3 path return 403. The Cloudflare CDN read path (armor-hub-b2.ardenone.com)
is dead post-hub-decommission. Direct R2 upload ensures replays are available
without waiting for a working B2 read path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EndpointResolverV2 with a custom static URI does not honor UsePathStyle —
the resolver provides the final endpoint and the SDK does not re-apply
path-style bucket addressing on top of it. This means the bucket name was
dropped from the request path even with UsePathStyle=true, sending PUTs
to /replays/... instead of /armor-apexalgo/replays/...
BaseEndpoint is the SDK's documented approach for S3-compatible custom
endpoints; it sets the base URL and then correctly applies path-style
addressing to produce /bucket/key URLs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes:
1. Add UsePathStyle=true to B2 S3 client. Without it the SDK uses
virtual-hosted addressing, dropping the bucket name from the request
path. Uploads hit /replays/... instead of /armor-apexalgo/replays/...
causing NoSuchBucket errors on every replay/thumbnail PutObject.
2. Don't update crash_strikes for normal game endings (stalemate, turns).
In snake-style games every bot eventually crashes into a wall/snake —
that is the expected end condition, not an HTTP error. The old code
treated all Crashed[] entries from the engine as errors, causing all
6 bots to accumulate strikes after every single match and hitting the
30-min cooldown threshold after just 3 matches.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The BaseEndpoint approach with older aws-sdk-go-v2 causes
"Invalid region: region was not a valid DNS name" errors when
uploading to ARMOR's S3-compatible endpoint.
Switching to EndpointResolverV2 bypasses the SDK's endpoint
rule validation entirely, resolving the issue.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fixes 'Invalid region: region was not a valid DNS name' error when
uploading replays to B2 via ARMOR proxy.
The error was caused by a known bug in aws-sdk-go-v2 v1.41.4 where
the endpoint resolver would validate the region as a DNS name even
when using a custom BaseEndpoint with UsePathStyle=true.
Upgraded SDK versions:
- github.com/aws/aws-sdk-go-v2 v1.41.4 -> v1.41.6
- github.com/aws/aws-sdk-go-v2/config v1.32.12 -> v1.32.16
- github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2 -> v1.100.0
- github.com/aws/smithy-go v1.24.2 -> v1.25.1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The AWS SDK requires a valid AWS region name even when using custom
S3-compatible endpoints (ARMOR/B2). Using "auto" as the region causes
an error: "Invalid region: region was not a valid DNS name."
This fixes the replay upload pipeline which was failing with the
invalid region error. Replays should now upload successfully to B2
via the ARMOR proxy.
Related to ai-code-battle-o43: Replay viewer verification task.
Remove custom endpoint resolver and use AWS SDK's standard approach
for S3-compatible endpoints:
- Use config.WithRegion("auto") for custom endpoints
- Set BaseEndpoint directly via s3.NewFromConfig options
- Add UsePathStyle for B2 compatibility
This fixes the 'Invalid region: region was not a valid DNS name' error
that was preventing replay uploads. The deployment manifest already
sets ACB_B2_REGION to empty string to avoid conflicts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Passing time.Duration (int64 nanoseconds) as $2 in NOW() + $2 caused
PostgreSQL to interpret the nanosecond value as seconds, setting
cooldown_until to year ~59066 instead of +30 minutes.
Fix: pre-compute time.Now().Add(CrashCooldownDuration) and pass the
resulting time.Time — pq encodes it as a proper timestamptz.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The seasons table was recreated with id BIGSERIAL (not season_id VARCHAR).
The ClaimJob query was still referencing s.season_id (stale column name).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Raw game scores (capture points) are tied in most matches since the
winner is determined by an energy/bots-alive tiebreaker. This caused
Glicko-2 delta=0, leaving rating_mu frozen at 1500 for all bots.
Now winner gets 1.0, non-winners 0.0, draws 0.5 — correct pairwise
win/loss signal for Glicko-2 convergence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The metrics package is a local module dependency imported by all services
but was missing from every Dockerfile's build context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace random 2-player pairing with the full §6.1 algorithm:
- Seed selection: bot with oldest last-match timestamp (tiebreak: lowest bot ID)
- Format selection: seed's least-played player count among {2, 3, 4, 6}
- Opponent selection: Pareto 80%/16-rank skill proximity + oldest last-pairing
with seed + fewest 24h games for game-count balance
- Map selection: least-recently-used active map for the chosen player count,
with map_scores.last_used_at updated after each match
- Random player slot assignment for all participant counts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PacifistBot never attacks; it survives by maximizing distance from enemies
and retreating toward own core when cornered. Pure evasion strategy that
wins via opponent elimination by third parties.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add crash_strikes and cooldown_until columns to bots table. Worker
increments strikes on crash (cooldown at 3), resets on success.
Matchmaker excludes cooldown bots from pairing, series scheduling,
and championship brackets. Fix erroneous cooldown filter on series
table in finalizeCompletedSeries (column only exists on bots).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The crash cooldown system was already implemented across engine, worker,
and matchmaker. This adds comprehensive integration tests that verify:
- Single crash does not trigger cooldown
- Two crashes do not trigger cooldown
- Three consecutive crashes trigger 30-min cooldown
- Successful match resets strike counter
- Interleaved crash/success resets counter correctly
- Cooldown extends on repeated crashes while on cooldown
- Matchmaker eligibility query excludes bots on active cooldown
- Matchmaker eligibility query includes bots with expired cooldown
- Full end-to-end flow: 3 crashes → excluded → cooldown expires → re-pair
Tests use ACB_TEST_DATABASE_URL env var for PostgreSQL integration tests
and skip gracefully when not configured.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Worker now gzip-compresses replays before uploading to B2 with
key replays/{match_id}.json.gz and Content-Encoding: gzip.
Updated B2 client Upload to accept contentEncoding parameter.
Fixed downstream web consumers (matches, bot-profile, playlists)
to reference .json.gz URLs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Worker resolves open predictions after writing match results (resolvePredictions + upsertPredictorStats)
- API endpoints: POST /api/predict, GET /api/predictions/open, GET /api/predictions/history
- Frontend /watch/predictions page with polling, prediction submission, and history display
- predictor_stats table tracks streaks and accuracy per predictor
- Series format selection: fix threshold from >200 to >=200 for bo3 eligibility
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Evolution page: live polling (10s), activity feed, candidate tracking,
statistics section, island overview with live.json schema
- Series page: detailed series view with game-by-game results
- Seasons page: season list with status and champion display
- Predictions page: enhanced prediction UI with open matches
- API types: add CycleInfo, Candidate, ActivityEntry, Totals for live.json
- Embed: improved embeddable replay widget
- Mobile CSS: responsive breakpoints and bottom tab bar
- Exporter: enhanced live.json generation with full cycle/candidate data
- Matchmaker: series scheduling support with config
- Worker: additional database queries for series/season data
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Upload replays to B2 (Backblaze) instead of R2 for cold archive storage
- Write match results directly to PostgreSQL instead of HTTP API
- Perform Glicko-2 rating updates in worker after match completion
- Update config: ACB_R2_* env vars → ACB_B2_*
- Remove obsolete api_test.go (tested removed HTTP client)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cleanup of superseded code that no longer matches the architecture:
Removed:
- worker-api/ - Cloudflare Worker with D1, superseded by K8s-based matchmaker + direct PostgreSQL
- cmd/acb-indexer/ - TypeScript index builder, superseded by Go cmd/acb-index-builder/
- cluster-configuration/ - K8s manifests belong in ardenone-cluster repo
Gutted cmd/acb-api/:
- Removed registration, job claim/result endpoints (deferred for v1)
- Removed dead code: predictions.go, seasons.go, series.go, register.go, jobs.go, glicko2.go
- API is now a stub with only health/ready endpoints
- Matchmaker and workers handle the core loop without it
Updated PROGRESS.md to reflect current architecture.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a metrics HTTP server to acb-worker exposing Prometheus text format
at /metrics, plus /health and /ready K8s probe endpoints. Tracks counters
(matches, errors, jobs, replays, polls, heartbeats) and histograms
(match duration, replay upload duration, replay size). Instruments the
full worker execution flow. Fixes .gitignore binary patterns to use
root-anchored paths so cmd/ subdirectories aren't incorrectly excluded.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add Dockerfile for acb-worker match execution container
- Add docker-compose.bots.yml for orchestrating all 6 strategy bots
- Add docker-compose.workers.yml for worker and indexer deployment
- Add .env.example documenting all required environment variables
- Add DEPLOYMENT.md with deployment guide and troubleshooting
- Update PROGRESS.md with Phase 6 progress
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Worker polls Cloudflare Worker API for pending match jobs
- Claims jobs and executes matches using the game engine
- Uploads replays to R2 via S3-compatible API
- Sends heartbeats during match execution
- Submits results back to Worker API
- Includes retry logic with exponential backoff
- API client tests for job coordination endpoints
Also fixes glicko2.ts: export g() and E() functions for testing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>