From 921b2a603dcdc83024e515aef47bba35027bcf47 Mon Sep 17 00:00:00 2001 From: Gavin McDonald Date: Sun, 11 Nov 2018 14:54:41 -0500 Subject: [PATCH] forking from https://gitea.daggertrout.com/mcdoh/ExMineArchive --- .formatter.exs | 4 + .gitignore | 24 +++++ README.md | 21 +++++ config/config.exs | 30 ++++++ lib/ex_mine.ex | 42 +++++++++ lib/ex_mine/application.ex | 18 ++++ lib/ex_mine/game.ex | 181 ++++++++++++++++++++++++++++++++++++ lib/ex_mine/parse_string.ex | 30 ++++++ lib/ex_mine/server.ex | 51 ++++++++++ lib/ex_mine/tile.ex | 35 +++++++ mix.exs | 30 ++++++ test/ex_mine_test.exs | 8 ++ test/test_helper.exs | 1 + 13 files changed, 475 insertions(+) create mode 100644 .formatter.exs create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config/config.exs create mode 100644 lib/ex_mine.ex create mode 100644 lib/ex_mine/application.ex create mode 100644 lib/ex_mine/game.ex create mode 100644 lib/ex_mine/parse_string.ex create mode 100644 lib/ex_mine/server.ex create mode 100644 lib/ex_mine/tile.ex create mode 100644 mix.exs create mode 100644 test/ex_mine_test.exs create mode 100644 test/test_helper.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..525446d --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..74e99b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +ex_mine-*.tar + diff --git a/README.md b/README.md new file mode 100644 index 0000000..1cb22a1 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# ExMine + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `ex_mine` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:ex_mine, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at [https://hexdocs.pm/ex_mine](https://hexdocs.pm/ex_mine). + diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..d371944 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,30 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Mix.Config module. +use Mix.Config + +# This configuration is loaded before any dependency and is restricted +# to this project. If another project depends on this project, this +# file won't be loaded nor affect the parent project. For this reason, +# if you want to provide default values for your application for +# 3rd-party users, it should be done in your "mix.exs" file. + +# You can configure your application as: +# +# config :ex_mine, key: :value +# +# and access this configuration in your application as: +# +# Application.get_env(:ex_mine, :key) +# +# You can also configure a 3rd-party app: +# +# config :logger, level: :info +# + +# It is also possible to import configuration files, relative to this +# directory. For example, you can emulate configuration per environment +# by uncommenting the line below and defining dev.exs, test.exs and such. +# Configuration from the imported file will override the ones defined +# here (which is why it is important to import them last). +# +# import_config "#{Mix.env}.exs" diff --git a/lib/ex_mine.ex b/lib/ex_mine.ex new file mode 100644 index 0000000..8a24bed --- /dev/null +++ b/lib/ex_mine.ex @@ -0,0 +1,42 @@ +defmodule ExMine do + + def new_game(args = %{}) do + {:ok, game_pid} = Supervisor.start_child(ExMine.Supervisor, [args]) + + game_pid + end + + def new_game(args) do + {:ok, new_args} = ParseString.parse(args) + + new_game(new_args) + end + + def get_height(game_pid) do + GenServer.call(game_pid, {:get_height}) + end + + def get_width(game_pid) do + GenServer.call(game_pid, {:get_width}) + end + + def get_board(game_pid) do + GenServer.call(game_pid, {:get_board}) + end + + def get_tile(game_pid, x, y) do + GenServer.call(game_pid, {:get_tile, x, y}) + end + + def get_state(game_pid) do + GenServer.call(game_pid, {:get_state}) + end + + def make_move(game_pid, x, y) do + GenServer.call(game_pid, {:make_move, x, y}) + end + + def toggle_flag(game_pid, x, y) do + GenServer.call(game_pid, {:toggle_flag, x, y}) + end +end diff --git a/lib/ex_mine/application.ex b/lib/ex_mine/application.ex new file mode 100644 index 0000000..d1c19cc --- /dev/null +++ b/lib/ex_mine/application.ex @@ -0,0 +1,18 @@ +defmodule ExMine.Application do + use Application + + def start(_type, _args) do + import Supervisor.Spec + + children = [ + worker(ExMine.Server, []) + ] + + options = [ + name: ExMine.Supervisor, + strategy: :simple_one_for_one, + ] + + Supervisor.start_link(children, options) + end +end diff --git a/lib/ex_mine/game.ex b/lib/ex_mine/game.ex new file mode 100644 index 0000000..92e242e --- /dev/null +++ b/lib/ex_mine/game.ex @@ -0,0 +1,181 @@ +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) do + board = Cartographer.new_board(height, width) + 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(:color, Map.put(tile.color, :alpha, tile.color.alpha / 10)) + + 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) + |> Map.put(:color, Map.put(tile.color, :alpha, tile.color.alpha / 10)) + + _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 diff --git a/lib/ex_mine/parse_string.ex b/lib/ex_mine/parse_string.ex new file mode 100644 index 0000000..30a290f --- /dev/null +++ b/lib/ex_mine/parse_string.ex @@ -0,0 +1,30 @@ +defmodule ParseString do + def parse(str) when is_binary(str)do + case str |> Code.string_to_quoted do + {:ok, terms} -> {:ok, _parse(terms)} + {:error, _} -> {:invalid_terms} + end + end + + # atomic terms + defp _parse(term) when is_atom(term), do: term + defp _parse(term) when is_integer(term), do: term + defp _parse(term) when is_float(term), do: term + defp _parse(term) when is_binary(term), do: term + + defp _parse([]), do: [] + defp _parse([h|t]), do: [_parse(h) | _parse(t)] + + defp _parse({a, b}), do: {_parse(a), _parse(b)} + defp _parse({:"{}", _place, terms}) do + terms + |> Enum.map(&_parse/1) + |> List.to_tuple + end + + defp _parse({:"%{}", _place, terms}) do + for {k, v} <- terms, into: %{}, do: {_parse(k), _parse(v)} + end + + defp _parse({_term_type, _place, terms}), do: terms # to ignore functions and operators +end diff --git a/lib/ex_mine/server.ex b/lib/ex_mine/server.ex new file mode 100644 index 0000000..03d0e86 --- /dev/null +++ b/lib/ex_mine/server.ex @@ -0,0 +1,51 @@ +defmodule ExMine.Server do + use GenServer + + alias ExMine.Game + + def start_link(args) do + GenServer.start_link(__MODULE__, args) + end + + def init(%{height: height, width: width}) do + {:ok, Game.new_game(height, width)} + end + + def handle_call({:get_height}, _from, game) do + {:reply, Game.get_height(game), game} + end + + def handle_call({:get_width}, _from, game) do + {:reply, Game.get_width(game), game} + end + + def handle_call({:get_board}, _from, game) do + {game, board} = Game.get_board(game) + {:reply, board, game} + end + + def handle_call({:get_tile, x, y}, _from, game) do + tile = Game.get_tile(game, x, y) + + {:reply, %{x: x, y: y, tile: tile}, game} + end + + def handle_call({:get_state}, _from, game) do + {game, state} = Game.get_state(game) + {:reply, state, game} + end + + def handle_call({:make_move, x, y}, _from, game) do + game = Game.make_move(game, x, y) + {game, board} = Game.get_board(game) + + {:reply, board, game} + end + + def handle_call({:toggle_flag, x, y}, _from, game) do + game = Game.toggle_flag(game, x, y) + tile = Game.get_tile(game, x, y) + + {:reply, %{x: x, y: y, tile: tile}, game} + end +end diff --git a/lib/ex_mine/tile.ex b/lib/ex_mine/tile.ex new file mode 100644 index 0000000..a7d300f --- /dev/null +++ b/lib/ex_mine/tile.ex @@ -0,0 +1,35 @@ +defmodule ExMine.Tile do + + defstruct( + color: %{ + red: 0, + green: 0, + blue: 0, + + alpha: 1.0, + }, + + mine: nil, + flag: false, + scale: 1.0, + unveiled: false, + update: -1, + neighboring_mines: nil + ) + + def new_tile(game, _x, _y) do + %__MODULE__{ + color: %{ + red: :rand.uniform(128) + 128, + green: :rand.uniform(128) + 128, + blue: :rand.uniform(128) + 128, + + alpha: (:rand.uniform(20) + 75) / 100, + }, + + mine: :rand.uniform() <= game.mine_density, + + scale: (:rand.uniform(20) + 70) / 100, + } + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..0eb485a --- /dev/null +++ b/mix.exs @@ -0,0 +1,30 @@ +defmodule ExMine.MixProject do + use Mix.Project + + def project do + [ + app: :ex_mine, + version: "0.1.0", + elixir: "~> 1.6", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + mod: {ExMine.Application, []}, + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:cartographer, path: "../cartographer"}, + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}, + ] + end +end diff --git a/test/ex_mine_test.exs b/test/ex_mine_test.exs new file mode 100644 index 0000000..2df2a52 --- /dev/null +++ b/test/ex_mine_test.exs @@ -0,0 +1,8 @@ +defmodule ExMineTest do + use ExUnit.Case + doctest ExMine + + test "greets the world" do + assert ExMine.hello() == :world + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()