lib/actions.ex

defmodule Tabletop.Actions do
  @moduledoc """
  This module provides the fundamental actions involved in any tabletop game. Each action
  consists of a key and then an arguments value:

    * `:move   => {from, to}`
    * `:add    => {piece, position}`
    * `:remove => position`
    * `:assign => {position, attributes}`

  A game will take a combination of these actions which will then form a turn. For more information
  see the `Tabletop.take_turn/2` function.
  """

  import Tabletop.Grid, only: [add: 2]
  import Kernel, except: [apply: 3]

  @doc """
  Applies a particular action to the `board`. The type of action is determined by the atom
  provided with `arg2`. This will then be mapped to a specific behaviour such as Moving
  or Adding a piece. If the type of action has not been implemented then the board will
  be returned with no changes made.

  ## Moving a Piece

    * `arg2`      => `:move`
    * `arguments` => `{from, to}`

  Moves the piece at the `from` position to the `to` position. Any existing pieces at the
  `to` position will be removed from the board and replaced by the moving piece.

    * `arg2` => `:step`
    * `arguments` => `{piece_id, direction}`

  Moves the first piece matching `piece_id` from its current position and in the provided
  `direction`.

  In the case that there is no piece to move, nothing will happen and the unchanged
  board struct will be returned.

  ## Adding a Piece

    * `arg2`     => `:add`
    * `arguments` => `{%Tabletop.Piece{}, position}`

  Adds the provided `piece` to the `position` on the `board`. Existing pieces will be
  removed from the `board` and replaced.

  If the `position` is not within the bounds of the `board`, the unchanged `board`
  will be returned.

  ## Removing a Piece

    * `arg2`     => `:remove`
    * `arguments` => `position`

  Removes the piece at the provided `position` if it is occupied. Does nothing to the `board`
  if the `position` does not contain a piece.

  ## Assigning Attributes to a Piece

    * `arg2`     => `:assign`
    * `arguments` => `%{}`

  Assigns the provided `attributes` to the piece at the provided `position`. If
  it does not exist then the unchanged `board` will be returned.
  """
  def apply(board, :move, {from, to}) do
    case Tabletop.get_piece(board, from) do
      %Tabletop.Piece{} = piece ->
        updated_pieces = Map.merge board.pieces, %{
          from => nil,
          to => piece
        }
        %Tabletop.Board{board | pieces: updated_pieces}
      nil ->
        board
    end
  end

  def apply(board, :add, {%Tabletop.Piece{} = piece, position}) do
    if Tabletop.in_bounds?(board, position) do
      updated_pieces = Map.merge(board.pieces, %{position => piece})
      %Tabletop.Board{board | pieces: updated_pieces}
    else
      board
    end
  end

  def apply(board, :remove, position) do
    if Tabletop.occupied?(board, position) do
      updated_pieces = Map.merge(board.pieces, %{position => nil})
      %Tabletop.Board{board | pieces: updated_pieces}
    else
      board
    end
  end

  def apply(board, :assign, {position, attributes}) do
    case Tabletop.get_piece(board, position) do
      %Tabletop.Piece{} = piece ->
        updated_pieces = Map.merge(board.pieces, %{
          position => Tabletop.Piece.assign(piece, attributes)
        })
        %Tabletop.Board{board | pieces: updated_pieces}
      nil ->
        board
    end
  end

  def apply(board, :step, {piece_id, direction}) do
    case Tabletop.position_of(board, Tabletop.Piece.new(piece_id)) do
      nil ->
        board
      position ->
        next_position = add(position, direction)
        if Tabletop.in_bounds?(board, next_position) do
          apply(board, :move, {position, next_position})
        else
          board
        end
    end
  end

  def apply(board, _key, _args) do
    board
  end

end