forking from https://gitea.daggertrout.com/mcdoh/ExMineArchive
This commit is contained in:
4
.formatter.exs
Normal file
4
.formatter.exs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
21
README.md
Normal 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
30
config/config.exs
Normal 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
42
lib/ex_mine.ex
Normal 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
|
||||||
18
lib/ex_mine/application.ex
Normal file
18
lib/ex_mine/application.ex
Normal 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
181
lib/ex_mine/game.ex
Normal 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
|
||||||
30
lib/ex_mine/parse_string.ex
Normal file
30
lib/ex_mine/parse_string.ex
Normal 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
51
lib/ex_mine/server.ex
Normal 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
35
lib/ex_mine/tile.ex
Normal 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
30
mix.exs
Normal 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
8
test/ex_mine_test.exs
Normal 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
1
test/test_helper.exs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ExUnit.start()
|
||||||
Reference in New Issue
Block a user