Cauldron2D.Client.Game behaviour (Cauldron2D v0.1.3)

Copy Markdown View Source

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 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/1nil 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

Summary

Functions

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

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

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

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

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

The words game gives for a refused join.

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

Types

action()

@type action() :: atom()

arena()

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

page()

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

rgb()

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

scene()

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

screen()

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

Callbacks

actions()

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

arenas(map)

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

atlas()

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

chat?()

(optional)
@callback chat?() :: boolean()

client_keys()

(optional)
@callback client_keys() :: %{required(atom()) => atom() | nil}

cue(term, screen)

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

frame_rates()

(optional)
@callback frame_rates() :: [:auto | pos_integer()]

guide()

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

holds()

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

home()

(optional)
@callback home() :: atom()

hud(term)

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

join_props(map)

(optional)
@callback join_props(map()) :: map()

keymaps()

@callback keymaps() ::
  %{required(atom()) => Cauldron2D.Input.keymap()}
  | [{atom(), Cauldron2D.Input.keymap()}]

listener(term)

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

lobby?()

(optional)
@callback lobby?() :: boolean()

map(term)

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

music()

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

outcome(term)

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

pages(map)

(optional)
@callback pages(map()) :: [page()]

player_label(term)

(optional)
@callback player_label(term()) :: String.t()

refusal_text(term)

(optional)
@callback refusal_text(term()) :: String.t()

scene(term)

@callback scene(term()) :: scene()

sink(map)

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

sounds()

@callback sounds() :: (atom() -> Cauldron2D.Audio.sound())

steering()

(optional)
@callback steering() :: [action()]

title()

@callback title() :: %{:name => String.t(), optional(:tagline) => String.t()}

toggles()

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

Functions

arena(game, props, id)

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

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

home(game)

@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(game)

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

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

optional(game, callback, args \\ [], default)

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

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

outcome(game, view)

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

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

refusal_text(game, reason)

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

The words game gives for a refused join.

static_map(game, view)

@spec static_map(module(), term()) :: %{
  width: pos_integer(),
  height: pos_integer(),
  cell: ({integer(), integer()} -> term() | nil),
  wrap?: boolean()
}

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