> For the complete documentation index, see [llms.txt](https://gotchi-battler-1.gitbook.io/gotchi-battler/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://gotchi-battler-1.gitbook.io/gotchi-battler/guides/fair-play.md).

# Fair Play and Battle Verification

Gotchi Battler's battle rules are completely open source. Anyone can inspect the [game logic on GitHub](https://github.com/Galaxy-Brain-Studios/gotchi-battler-game-logic), check the source of a battle's randomness, and run the calculation themselves to check every turn and the final result.

You don't need to do any of this to play. This guide explains how it works, with an optional technical walkthrough for players who want to check a battle themselves.

## How a battle gets its result

Three ingredients determine a battle:

1. **The starting teams:** each unit's stats, abilities, equipment, position, and starting condition.
2. **The battle rules:** the exact version of the game logic used for that battle.
3. **The random seed:** a value that determines the battle's random choices, such as targets and critical hits.

With the same three ingredients, the game logic produces the same sequence of turns and the same result. Watching a replay shows those turns visually; running the open-source game logic lets you independently check the calculation behind them.

## Where the randomness comes from

Gotchi Battler uses **drand**, an independent network that publishes publicly verifiable random values. Its [Quicknet network](https://docs.drand.love/blog/2023/10/16/quicknet-is-live/) publishes a new value every three seconds. Each publication has a numbered **round** and a digital signature that anyone can check.

The game selects a future drand round using a fixed timing rule, waits for that round's value, and verifies its signature before using it. For tournaments, the selection uses the scheduled start of the tournament round. For individual plays, it uses the recorded play time. This makes it possible to check which drand value belongs to a battle.

A drand round and a tournament round are different things: one is a publication of randomness, and the other is a stage of the tournament.

## What you can check

* **The seed's source:** does it match the signed value from the expected drand round?
* **The battle calculation:** do the recorded starting teams, rules, and seed reproduce every turn and the final result?
* **The rules themselves:** how do attacks, abilities, targeting, and other battle mechanics work?

These checks establish that the recorded battle follows the published rules and uses the expected public randomness. They rely on the starting teams and timing information in the game record. A saved play time alone does not independently prove when the game committed to those inputs; that would require a separate advance record.

## Get your battle log

Open a battle replay in the game and select **Logs** to download `logs.json`. Keep the original file: it records the seed, the game-logic version, the starting units and formation, the turns, and the result.

For Crystal Temple runs, the download contains a `battles` list, with a separate log for each encounter. Each encounter must be checked using its own starting condition, including any remaining health, statuses, and Special charge.

## Advanced: verify a battle yourself

The steps below use Node.js and the public game-logic package. They check completed battles that use drand and include the current log format. Earlier Beta battles can use different randomness or log formats, and Training simulations use local randomness.

### 1. Find the time used to select the seed

Use the relevant timestamp from the saved game record:

| Battle         | Timestamp to use                                                                 |
| -------------- | -------------------------------------------------------------------------------- |
| Tournament     | The tournament round's scheduled `startDate`, even if processing finished later. |
| Campaign       | The play's `createdAt`.                                                          |
| Quest          | The play's `createdAt`.                                                          |
| Crystal Temple | The play's `playedAt`, which records when the run started.                       |

The downloaded battle log alone does not contain this timing record. If it is not shown in the game, you can inspect the saved play or tournament round response in your browser's developer tools, under **Network**. Use the record for the battle you are checking.

**Do not use `meta.timestamp` from the battle log:** that is the time the log was generated. For the Crystal Temple, do not use the ticket's creation time either.

Convert the timestamp to whole Unix seconds, rounding down. For example, in JavaScript: `Math.floor(Date.parse(timestamp) / 1000)`.

The selection rule is:

```
drandRound = floor((unixSeconds - 1692803367) / 3) + 3
```

This selects a drand publication scheduled roughly three to six seconds after the recorded time. The game uses the full lowercase hexadecimal `randomness` value, without a `0x` prefix. Crystal Temple encounters append `:crystal-temple:battle:N`, where `N` is the encounter's `index`, starting at 1.

### 2. Install the matching rules

Read `meta.gameLogicVersion` in your log (inside each encounter's `logs` for a Temple run). In a new folder, install that **exact** version of `gotchi-battler-game-logic`, along with `drand-client`.

For example, for a log recorded with version `5.0.7`:

```sh
npm init -y
npm install --save-exact gotchi-battler-game-logic@5.0.7 drand-client@1.4.2
```

Copy your `logs.json` into that folder. Using the latest rules for an older battle can change the result, so always match the recorded version.

### 3. Check the seed and rerun the battle

Save the following as `verify-battle.cjs` in the same folder. It uses the [official drand client](https://github.com/drand/drand-client) to verify the signature against Quicknet's public key, checks the expected seed, and compares all turns and the final result with a fresh calculation.

```js
const assert = require('node:assert/strict')
const fs = require('node:fs')
const { HttpCachingChain, HttpChainClient, fetchBeacon } = require('drand-client')
const { battle, createBattleInputFromLog } = require('gotchi-battler-game-logic')
const { version } = require('gotchi-battler-game-logic/package.json')

async function verify() {
    const [file, time] = process.argv.slice(2)
    const unixSeconds = Number(time)
    assert(Number.isSafeInteger(unixSeconds) && unixSeconds >= 1692803367,
        'Supply the recorded time as whole Unix seconds')
    const download = JSON.parse(fs.readFileSync(file, 'utf8'))
    const isTempleRun = Array.isArray(download.battles)
    const encounters = isTempleRun ? download.battles : [{ logs: download }]
    assert(encounters.length > 0, 'No battles found')

    const chainHash = '52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971'
    const publicKey = '83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a'
    const options = {
        disableBeaconVerification: false,
        chainVerificationParams: { chainHash, publicKey }
    }
    const chain = new HttpCachingChain(`https://api.drand.sh/${chainHash}`, options)
    const client = new HttpChainClient(chain, options)
    const round = Math.floor((unixSeconds - 1692803367) / 3) + 3
    const beacon = await fetchBeacon(client, round)
    assert.equal(beacon.round, round, 'Wrong drand round')
    const seed = Buffer.from(beacon.randomness, 'hex').toString('hex')
    const json = value => JSON.parse(JSON.stringify(value))

    for (const encounter of encounters) {
        const log = encounter.logs
        assert.equal(log.meta.gameLogicVersion, version, 'Install the recorded game-logic version')
        if (isTempleRun) assert(Number.isInteger(encounter.index) && encounter.index >= 1)
        const expectedSeed = isTempleRun
            ? `${seed}:crystal-temple:battle:${encounter.index}`
            : seed
        assert.equal(log.meta.seed, expectedSeed, 'Seed does not match the expected drand round')
        const input = createBattleInputFromLog(log, {
            mode: 'rebased',
            preserveStartingState: true
        })
        const replay = battle(input.team1, input.team2, input.seed, input.options)
        assert.deepEqual(json(replay.turns), log.turns, 'Battle turns differ')
        assert.deepEqual(json(replay.result), log.result, 'Battle result differs')
    }

    console.log(`Verified drand round ${round} and ${encounters.length} battle(s).`)
}

verify().catch(error => {
    console.error(error.message)
    process.exitCode = 1
})
```

Run it with your recorded time, replacing `YOUR_UNIX_TIMESTAMP` with the number from step 1:

```sh
node verify-battle.cjs logs.json YOUR_UNIX_TIMESTAMP
```

A successful check confirms the signed seed and reproduces the recorded turns and result. The newly generated log has a different creation timestamp, so the script compares the battle content instead of requiring the entire file to be identical.

For a Temple run, this checks each included battle from its recorded starting condition. Checking the full run's progression also means checking that each encounter starts with the previous encounter's ending condition; reward selection is a separate calculation.

If a check fails, first confirm the timestamp, game-logic version, and original download. A missing `setup.statAdjustments` error means the log does not contain the setup information this reconstruction method needs. Do not bypass a failed check or substitute guessed inputs: keep the log and share the error when asking for help in [Discord](https://discord.gg/vvDZENEyXY).
