# `Cauldron2D.Client.Game`
[🔗](https://github.com/jaman/cauldron/blob/v0.1.3/cauldron_2d/lib/cauldron_2d/client/game.ex#L1)

What a game gives a client — the terminal's, the browser's or the desktop's — to put
its worlds in front of a player. One contract; every front end draws from it.

    defmodule MyGame.Client do
      @behaviour Cauldron2D.Client.Game

      def title, do: %{name: "My Game", tagline: "a thing with paddles"}
      def atlas, do: :my_game
      def actions, do: [:left, :right, :launch]
      def keymaps, do: [arrows: %{left: [:left], right: [:right], launch: [:" "]}, wasd: %{...}]
      def toggles, do: []
      def arenas(_props), do: Cauldron2D.Arenas.list()
      def scene(view), do: %{focus: view.me.pos, bounds: view.bounds, cell: &MyGame.cell/1, movers: view.movers, labels: []}
      def hud(view), do: [["Balls ", {Integer.to_string(view.me.balls), {255, 220, 0}}], [{:score, ["Score #{view.me.score}"]}]]
      def listener(view), do: view.me.pos
      def sounds, do: &MyGame.Sound.voice/1
      def music, do: [{"meadow", %{bpm: 112, beats_per_bar: 4, layers: [...], sections: %{...}}}]
      def cue(view, :arena), do: %{piece: "meadow", section: view.tension, layers: %{}}
      def cue(_view, _screen), do: nil
    end

A view is whatever the game's `c:Cauldron2D.Game.view/2` returned for this player;
`scene/1`, `hud/1`, `listener/1`, `outcome/1` and `cue/2` read it; none of them is
asked before an arena's first frame has arrived. The hud is data in
the shapes `Cauldron2D.Client.Hud` describes. `scene/1`'s `:labels` are text drawn over
the world as `Cauldron2D.Surface.compose/4` takes them, in tiles; default none. Its
`:subject` names what the focus follows — the player's own ship, the ship they watch,
a free camera — as a string, atom or number; a client that has panned the view drops
the pan when the subject changes, so a new ship comes up centred.

`arenas/1` is given the client's props and read whenever a lobby draws, so it may
answer differently each time. An arena is `%{id, name, world, players, humans, map,
note, kind, teams}` as `Cauldron2D.Arenas.list/0` gives it: `world` is the name a
player joins, `{module, opts}` for a world the client starts itself for this player
alone (as `Cauldron2D.World.start_link/1` takes them, the client adding `:name`),
`players` how many humans are in it and `humans` their ids where the lister knows
them, `kind` `{text, rgb}`, `teams` the choices a player may join as (prop `:team`).
A lobby reads an arena's players from the listing alone; it never speaks to the
world, whose name may start it.

The lobby is the screen that lists the arenas — the worlds — and lets a player pick
one, a team in it, or watching; it has nothing to do with how the world steps, and a
turn-based game whose players share a world keeps it. Who a player is once in the
world — a character, a party, a side — is the game's: chosen through `join_props/1`,
on a page of its own from `pages/1`, or in the world itself. A game with `lobby?/0`
false has no arena to choose; its client goes from the title straight into its first
arena, and back to the title from it.

`cue/2`'s second argument is the screen — `:title`, `:lobby`, `:settings`, `:arena`,
`:summary` or `:guide` — and `nil` means no music. On `:summary` the view is the last
one the arena showed. A client whose music is static (`Cauldron2D.Audio` with
`static_music: true`, as a server's `Cauldron2D.Net.Audio` policy `:static` sets) asks
`cue(view, {:static, screen})` first — the one section it will loop for the whole
stay, so a game can answer with a piece written as a loop rather than the layered
one that follows the play — and falls back to `cue(view, screen)` when that is `nil`.

## Optional callbacks

  * `holds/0` — actions a terminal without key releases keeps held through its key-repeat
    delay on a press, with the milliseconds each is held, as `Cauldron2D.Input`'s
    `:holds`
  * `steering/0` — the actions the pointer also drives
  * `outcome/1` — `nil` while play goes on, or `%{title, lines, next}` when it is over
    for this player; `next` is `:next_arena`, `:same_arena` or `:lobby` (the default)
  * `join_props/1` — extra props for the world's join, from `%{username, settings,
    params}` (`params` a browser's join params, absent elsewhere)
  * `map/1` — the static layer a browser or window draws once, `%{width, height, cell,
    wrap?}` with `cell` a tile to an art or `nil`; without it, the layer is read from
    `scene/1`'s bounds and cell, the topmost art of each tile
  * `guide/0` — sections of entries, each `%{art, name, text}`
  * `sink/1` — the `TuningFork.Sink` this player's audio plays through, from the props
  * `pages/1` — extra pages reachable from the title, from the props: `[%{key, hint,
    title, render}]`, `render` a function returning rows of runs
  * `refusal_text/1` — the words a refused join is reported with
  * `player_label/1` — how a player id is written in a lobby
  * `client_keys/0` — the client's own keys, as the terminal client documents them
  * `chat?/0` — whether the lobby and arena carry a chat; `true` without it
  * `frame_rates/0` — the frame rates a settings screen offers
  * `lobby?/0` — whether a screen of arenas stands between the title and play; `true`
    without it
  * `home/0` — the name the game keeps its files under, as `Cauldron2D.Paths` takes it:
    rendered music goes to `Cauldron2D.Paths.cache(home, "music")`. Without it, the
    game's module name underscored, up to its first dot: `MyGame.Client` is `:my_game`

# `action`

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

# `arena`

```elixir
@type arena() :: %{
  :id =&gt; term(),
  :name =&gt; String.t(),
  :world =&gt; GenServer.server() | {module(), keyword()},
  optional(:players) =&gt; non_neg_integer(),
  optional(:humans) =&gt; [term()],
  optional(:map) =&gt; String.t(),
  optional(:note) =&gt; String.t(),
  optional(:kind) =&gt; {String.t(), rgb()},
  optional(:teams) =&gt; [term()]
}
```

# `page`

```elixir
@type page() :: %{
  key: atom(),
  hint: String.t(),
  title: String.t(),
  render: (-&gt; [Cauldron2D.Client.Hud.row()])
}
```

# `rgb`

```elixir
@type rgb() :: {byte(), byte(), byte()}
```

# `scene`

```elixir
@type scene() :: %{
  :focus =&gt; {number(), number()},
  :bounds =&gt; {pos_integer(), pos_integer()} | :unbounded,
  :cell =&gt; ({integer(), integer()} -&gt; term()),
  :movers =&gt; [{term(), {number(), number()}}],
  optional(:labels) =&gt; list(),
  optional(:subject) =&gt; String.t() | atom() | number()
}
```

# `screen`

```elixir
@type screen() ::
  :title | :lobby | :settings | :arena | :summary | :guide | {:static, atom()}
```

# `actions`

```elixir
@callback actions() :: [action()]
```

# `arenas`

```elixir
@callback arenas(map()) :: [arena()]
```

# `atlas`

```elixir
@callback atlas() :: Cauldron2D.Atlas.name()
```

# `chat?`
*optional* 

```elixir
@callback chat?() :: boolean()
```

# `client_keys`
*optional* 

```elixir
@callback client_keys() :: %{required(atom()) =&gt; atom() | nil}
```

# `cue`

```elixir
@callback cue(term(), screen()) :: Cauldron2D.Audio.Music.cue() | nil
```

# `frame_rates`
*optional* 

```elixir
@callback frame_rates() :: [:auto | pos_integer()]
```

# `guide`
*optional* 

```elixir
@callback guide() :: [{String.t(), [%{art: term(), name: String.t(), text: String.t()}]}]
```

# `holds`
*optional* 

```elixir
@callback holds() :: [{action(), pos_integer()}]
```

# `home`
*optional* 

```elixir
@callback home() :: atom()
```

# `hud`

```elixir
@callback hud(term()) :: Cauldron2D.Client.Hud.t()
```

# `join_props`
*optional* 

```elixir
@callback join_props(map()) :: map()
```

# `keymaps`

```elixir
@callback keymaps() ::
  %{required(atom()) =&gt; Cauldron2D.Input.keymap()}
  | [{atom(), Cauldron2D.Input.keymap()}]
```

# `listener`

```elixir
@callback listener(term()) :: {number(), number()} | nil
```

# `lobby?`
*optional* 

```elixir
@callback lobby?() :: boolean()
```

# `map`
*optional* 

```elixir
@callback map(term()) :: %{
  :width =&gt; pos_integer(),
  :height =&gt; pos_integer(),
  :cell =&gt; ({integer(), integer()} -&gt; term() | nil),
  optional(:wrap?) =&gt; boolean()
}
```

# `music`

```elixir
@callback music() :: [{String.t(), Cauldron2D.Audio.Music.spec()}]
```

# `outcome`
*optional* 

```elixir
@callback outcome(term()) ::
  nil
  | %{
      :title =&gt; String.t(),
      :lines =&gt; [String.t()],
      optional(:next) =&gt; :next_arena | :same_arena | :lobby
    }
```

# `pages`
*optional* 

```elixir
@callback pages(map()) :: [page()]
```

# `player_label`
*optional* 

```elixir
@callback player_label(term()) :: String.t()
```

# `refusal_text`
*optional* 

```elixir
@callback refusal_text(term()) :: String.t()
```

# `scene`

```elixir
@callback scene(term()) :: scene()
```

# `sink`
*optional* 

```elixir
@callback sink(map()) :: module() | {module(), keyword()}
```

# `sounds`

```elixir
@callback sounds() :: (atom() -&gt; Cauldron2D.Audio.sound())
```

# `steering`
*optional* 

```elixir
@callback steering() :: [action()]
```

# `title`

```elixir
@callback title() :: %{:name =&gt; String.t(), optional(:tagline) =&gt; String.t()}
```

# `toggles`

```elixir
@callback toggles() :: [action()]
```

# `arena`

```elixir
@spec arena(module(), map(), String.t()) :: arena() | nil
```

The arena whose id writes as `id`, from `game`'s arenas for `props`.

# `home`

```elixir
@spec home(module()) :: atom()
```

The name the game keeps its files under: its `home/0`, else its module name's first part underscored.

# `music_dir`

```elixir
@spec music_dir(module()) :: Path.t()
```

Where the game's rendered music is kept: `Cauldron2D.Paths.cache(home, "music")`.

# `optional`

```elixir
@spec optional(module(), atom(), [term()], term()) :: term()
```

Call an optional callback of `game` with `args`, or `default` when it has none.

# `outcome`

```elixir
@spec outcome(module(), term()) :: map() | nil
```

The player's outcome for `view`, `nil` while play goes on.

# `refusal_text`

```elixir
@spec refusal_text(module(), term()) :: String.t()
```

The words `game` gives for a refused join.

# `static_map`

```elixir
@spec static_map(module(), term()) :: %{
  width: pos_integer(),
  height: pos_integer(),
  cell: ({integer(), integer()} -&gt; term() | nil),
  wrap?: boolean()
}
```

The static layer for `view`: the game's `map/1`, else read from its scene's bounds and cell.

---

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