defmodule ExMine.Game do defstruct( board: nil, flags: 0, # how many times a user has flagged or unflagged mine_density: 0.20625, start_time: 0, state: :initializing, # tracks moves # used to return only updated tiles to user # start at -1 as the first 'update' is to report the board to the user update: -1 ) alias ExMine.Tile @report_to_user [:play_time, :state, :update] def new_game(height, width, wrap \\ :wrap) do board = Cartographer.new_board(height, width, wrap) tiles = for x <- 0..(width - 1), y <- 0..(height - 1), do: {x, y} Enum.reduce(tiles, %ExMine.Game{board: board}, fn({x, y}, game) -> new_tile(game, x, y) end) end def get_height(game), do: Cartographer.height(game.board) def get_width(game), do: Cartographer.width(game.board) def get_board(game) do tiles = game.board.tiles |> Enum.filter(fn({_key, tile}) -> tile.update == game.update end) |> Enum.map(fn({{x, y}, tile}) -> {"#{ x },#{ y }", scrub_tile(tile)} end) |> Enum.into(%{}) game = Map.put(game, :update, game.update + 1) {game, %{tiles: tiles, height: game.board.height, width: game.board.width}} end def get_tile(game, x, y) do Cartographer.get(game.board, x, y) |> scrub_tile() end def get_state(game = %{state: :lost}), do: {game, Map.take(game, @report_to_user)} def get_state(game = %{state: :won}), do: {game, Map.take(game, @report_to_user)} def get_state(game) do won = game.board.tiles |> Enum.all?(fn({_key, tile}) -> tile.unveiled or tile.mine end) game = if won do Map.put(game, :state, :won) else game end game = Map.put(game, :play_time, Time.diff(Time.utc_now(), game.start_time, :microsecond)) {game, Map.take(game, @report_to_user)} end def make_move(game, x, y) do unveil(game, x, y, Cartographer.get(game.board, x, y)) end def toggle_flag(game, x, y) do toggle_flag(game, x, y, Cartographer.get(game.board, x, y)) end ############################################################################################## defp new_tile(game, x, y) do board = Cartographer.set(game.board, x, y, Tile.new_tile(game, x, y)) Map.put(game, :board, board) end defp scrub_tile(tile = %{unveiled: true}), do: Map.drop(tile, [:update]) defp scrub_tile(tile), do: Map.drop(tile, [:mine, :update]) defp unveil(game, _x, _y, _tile = nil), do: game defp unveil(game, _x, _y, _tile = %{flag: true}), do: game defp unveil(game, _x, _y, _tile = %{unveiled: true}), do: game defp unveil(game = %{state: :initializing}, x, y, _tile) do # make sure the opening move neighbors zero mines board = game.board |> Cartographer.get(x, y, 1) |> Enum.reduce(%{}, fn({{x, y}, tile}, nerfed) -> Map.put(nerfed, {x, y}, Map.put(tile, :mine, false)) end) |> Enum.reduce(game.board, fn({{x, y}, tile}, board) -> Cartographer.set(board, x, y, tile) end) game = game |> Map.put(:board, board) |> Map.put(:start_time, Time.utc_now()) |> Map.put(:state, :playing) unveil(game, x, y, Cartographer.get(game.board, x, y)) end defp unveil(game, x, y, tile) do tile = tile |> Map.put(:update, game.update) |> Map.put(:unveiled, true) |> Map.put(:unveiling, 0.0) check_tile(game, x, y, tile) end defp check_tile(game, x, y, tile = %{mine: true}), do: update_tile(game, x, y, tile) defp check_tile(game, x, y, tile = %{neighboring_mines: neighboring_mines}) when not is_nil(neighboring_mines), do: update_tile(game, x, y, tile) defp check_tile(game, x, y, tile) do neighboring_mines = game.board |> Cartographer.neighbors(x, y) |> Enum.count(fn({_xy, tile}) -> tile.mine end) tile = tile |> Map.put(:update, game.update) |> Map.put(:neighboring_mines, neighboring_mines) update_tile(game, x, y, tile) end defp check_neighbors(game, x, y) do _check_neighbors(game, x, y, Cartographer.get(game.board, x, y)) end defp _check_neighbors(game, _x, _y, _tile = %{mine: true}), do: game defp _check_neighbors(game, _x, _y, _tile = %{neighboring_mines: neighboring_mines}) when neighboring_mines != 0, do: game defp _check_neighbors(game, x, y, _tile) do game.board |> Cartographer.neighbors(x, y) |> Enum.filter(fn({{_x, _y}, tile}) -> not tile.mine end) |> Enum.filter(fn({{_x, _y}, tile}) -> is_nil(tile.neighboring_mines) end) |> Enum.reduce(game, fn({{x, y}, tile}, game) -> check_tile(game, x, y, tile) end) end defp update_tile(game, x, y, tile = %{mine: true, unveiled: true}) do game |> Map.put(:play_time, Time.diff(Time.utc_now(), game.start_time, :microsecond)) |> Map.put(:state, :lost) |> _update_tile(x, y, tile) end defp update_tile(game, x, y, tile = %{mine: false, neighboring_mines: neighboring_mines, unveiled: false}) when not is_nil(neighboring_mines) do tile = tile |> Map.put(:update, game.update) |> Map.put(:unveiled, true) _update_tile(game, x, y, tile) end defp update_tile(game, x, y, tile) do _update_tile(game, x, y, tile) end defp _update_tile(game, x, y, tile) do board = Cartographer.set(game.board, x, y, tile) game |> Map.put(:board, board) |> check_neighbors(x, y) end defp toggle_flag(game, _x, _y, _tile = %{unveiled: true}), do: game defp toggle_flag(game, x, y, tile) do tile = Map.put(tile, :flag, not tile.flag) board = Cartographer.set(game.board, x, y, tile) game |> Map.put(:flags, game.flags + 1) |> Map.put(:board, board) end end