# `Cauldron2D.Ledger`
[🔗](https://github.com/jaman/cauldron/blob/v0.2.2/cauldron_2d/lib/cauldron_2d/ledger.ex#L1)

Every player's results, kept on disk, and the boards made from them.

    {:ok, ledger} = Cauldron2D.Ledger.start_link(path: Cauldron2D.Paths.data(:my_game, "ledger.dets"), metrics: [kills: {:sum, :kills}, ratio: {:ratio, :kills, :deaths}, wins: {:count, :won?}])
    Cauldron2D.Ledger.record(ledger, "Pit", %{id: "alice", name: "alice", kills: 3, deaths: 1, won?: true})
    Cauldron2D.Ledger.board(ledger, :week, :kills, world: "Pit")

A result is a map with the player's account as `:id` (their `:name` when there is no
id), the name they played under as `:name`, and whatever fields the metrics read. A
result whose id is `{:robot, n}` is not kept. `Cauldron2D.Ledger.Recorder` takes
them from a world's events.

## Metrics

The game declares its metrics as a keyword list, each one of:

  * `{:sum, field}` — the field summed over the rounds
  * `{:max, field}` — the field's largest value
  * `{:count, field}` — how many rounds had the field true
  * `{:ratio, field, by}` — the sum of `field` over the sum of `by`, to two places;
    the sum of `field` alone when `by` sums to zero
  * a function of the rounds returning a number

A board is the accounts of a period — `:day` (since midnight UTC), `:week` (the last
seven days) or `:all` — ranked by one metric, each with every metric's value, the
rounds counted and the name last played under, twenty at most. Boards are computed from
the table and kept for ten seconds.

## The file

The file belongs to one operating system process at a time, held through
`Cauldron2D.Ledger.Lock`. Starting a ledger on a held file fails with
`{:error, {:held, pid}}`, or with `held: :idle` starts one that keeps nothing: its
boards are empty, `record/4` and `forget/2` answer `{:error, {:held, pid}}`, and
`access/1` says `{:held, pid}`. A DETS failure stops the ledger, and a restart reopens
the file repaired.

# `entry`

```elixir
@type entry() :: %{
  name: String.t(),
  account: term(),
  value: number(),
  rounds: pos_integer(),
  values: %{required(metric()) =&gt; number()}
}
```

# `metric`

```elixir
@type metric() :: atom()
```

# `period`

```elixir
@type period() :: :day | :week | :all
```

# `access`

```elixir
@spec access(GenServer.server()) :: :read_write | {:held, pos_integer()}
```

Whether the ledger holds its file: `:read_write`, or `{:held, pid}` naming the operating system process that does.

# `board`

```elixir
@spec board(GenServer.server(), period(), metric(), keyword()) :: [entry()]
```

The board of `period` by `metric`: the top 20, or `top: n` of them, or every account with `top: :all`; `world:` narrows it to one world.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `count`

```elixir
@spec count(GenServer.server()) :: non_neg_integer()
```

How many results are kept.

# `forget`

```elixir
@spec forget(GenServer.server(), (map() -&gt; boolean())) ::
  {:ok, non_neg_integer()} | {:error, term()}
```

Drop every result `keep_out?` is true of — a function of the row as it was recorded, with its `:label` (the world name) and `:at` — and say how many went.

# `holder`

```elixir
@spec holder(Path.t()) :: pos_integer() | nil
```

The operating system process holding the file at `path`, or nil when it is free.

# `metrics`

```elixir
@spec metrics(GenServer.server()) :: [metric()]
```

The metrics the ledger was started with, in order.

# `record`

```elixir
@spec record(GenServer.server(), String.t(), map(), DateTime.t()) ::
  :ok | {:error, term()}
```

Keep a result from `world`, taken `at` (default now).

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Start the ledger.

## Options

  * `:path` — the DETS file. Required
  * `:metrics` — the metrics, as the module documentation describes. Required
  * `:name` — the registered name; default the module, `nil` for none
  * `:held` — what to do when another process holds the file: `:stop` (default) or `:idle`

---

*Consult [api-reference.md](api-reference.md) for complete listing*
