feat: publish notable fight results to Nostr relays

NIP-01 kind:1 notes posted for KOs, perfects, upsets, and big Elo
swings. Includes bot names, Elo changes, arena, and replay link.
Configurable via BOTFIGHTS_NOSTR_RELAYS and BOTFIGHTS_NOSTR_NSEC env
vars. Silently skips if NSEC not set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 20:01:11 +00:00
co-authored by Claude Opus 4.6
parent 5e598e3987
commit 7761f713d7
2 changed files with 108 additions and 0 deletions
+26
View File
@@ -11,6 +11,7 @@ import { setCooldown } from './queue.js'
import { isHumanPlayer, waitForHumanResponse } from './human-responses.js'
import { payWinner, refundEntry, ENTRY_FEE_SATS } from './payments.js'
import { settleBets, lockBets } from './betting.js'
import { publishFightResult } from './nostr-publish.js'
interface BotRecord {
id: string
@@ -333,12 +334,14 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec
let comboA = 0
let comboB = 0
let winnerId: string | null = null
let lastRound = 0
const usedTypes = new Set<string>()
// Pick a random round for retro mode (rounds 3-8, ensuring it's not too early or late)
const retroRound = 3 + Math.floor(Math.random() * Math.min(6, MAX_ROUNDS - 4))
for (let round = 1; round <= MAX_ROUNDS; round++) {
lastRound = round
const challenge = round === retroRound
? generateRetroChallenge()
: pickChallenge(usedTypes, arena.modifier)
@@ -447,6 +450,10 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec
// Finalize fight + update bot stats atomically
const isMockFight = isMockBot(botA.webhookUrl) || isMockBot(botB.webhookUrl) || isClassicBot(botA.webhookUrl) || isClassicBot(botB.webhookUrl)
const kFactor = isMockFight ? 12 : 32 // Dampened Elo for mock/classic fights
let winnerEloChange = 0
let loserEloChange = 0
let newWinnerEloFinal = 0
let newLoserEloFinal = 0
const finalize = sqlite.transaction(() => {
// Mark fight finished
@@ -463,6 +470,10 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec
const loser = winnerId === botA.id ? botB : botA
const { newWinnerElo, newLoserElo } = calculateElo(winner.eloRating, loser.eloRating, kFactor)
winnerEloChange = Math.round(newWinnerElo - winner.eloRating)
loserEloChange = Math.round(newLoserElo - loser.eloRating)
newWinnerEloFinal = newWinnerElo
newLoserEloFinal = newLoserElo
const newWinStreak = winner.winStreak + 1
const newBestStreak = Math.max(winner.bestStreak, newWinStreak)
@@ -510,6 +521,21 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec
potSats: mode === 'ranked' ? 42 : 0,
})
// Publish notable results to Nostr
if (winnerId) {
const winner = winnerId === botA.id ? botA : botB
const loser = winnerId === botA.id ? botB : botA
const isUpset = loser.eloRating - winner.eloRating > 150
const isKO = (winnerId === botA.id && hpB <= 0) || (winnerId === botB.id && hpA <= 0)
publishFightResult({
fightId, winnerName: winner.name, loserName: loser.name, winnerId,
winnerElo: newWinnerEloFinal, loserElo: newLoserEloFinal,
winnerEloChange, loserEloChange,
isPerfect: !!isPerfect, isKO, isUpset,
totalRounds: lastRound, arena: arena.name,
}).catch(err => console.warn('[nostr] publish failed:', err))
}
// Settle bets
try {
const settlements = await settleBets(fightId, winnerId)