This commit is contained in:
Gavin McDonald
2018-11-11 14:54:41 -05:00
commit 921b2a603d
13 changed files with 475 additions and 0 deletions

4
.formatter.exs Normal file
View File

@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
.gitignore vendored Normal file
View File

@@ -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

21
README.md Normal file
View File

@@ -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).

30
config/config.exs Normal file
View File

@@ -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"

42
lib/ex_mine.ex Normal file
View File

@@ -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

View File

@@ -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

181
lib/ex_mine/game.ex Normal file
View File

@@ -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

View File

@@ -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

51
lib/ex_mine/server.ex Normal file
View File

@@ -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

35
lib/ex_mine/tile.ex Normal file
View File

@@ -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

30
mix.exs Normal file
View File

@@ -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

8
test/ex_mine_test.exs Normal file
View File

@@ -0,0 +1,8 @@
defmodule ExMineTest do
use ExUnit.Case
doctest ExMine
test "greets the world" do
assert ExMine.hello() == :world
end
end

1
test/test_helper.exs Normal file
View File

@@ -0,0 +1 @@
ExUnit.start()