├── .gitignore ├── .tool-versions ├── .travis.yml ├── LICENSE ├── README.md ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── lib ├── dwarlixir.ex └── dwarlixir │ ├── application.ex │ ├── components │ ├── age.ex │ ├── container.ex │ ├── dead.ex │ └── mortal.ex │ ├── connections │ ├── ranch_handler.ex │ └── tcp.ex │ ├── controllers │ ├── generic_mob_controller.ex │ ├── human │ │ ├── impl.ex │ │ └── server.ex │ ├── human_controller.ex │ └── human_old.ex │ ├── ecosystem │ └── ecosystem.ex │ ├── entities │ ├── bird.ex │ ├── dwarf.ex │ ├── location.ex │ └── player_character.ex │ ├── item │ ├── corpse.ex │ ├── egg.ex │ └── supervisor.ex │ ├── life │ ├── reaper.ex │ └── timers.ex │ ├── mobs │ ├── mob_template.ex │ └── sexual_reproduction.ex │ ├── systems │ ├── aging.ex │ ├── death.ex │ └── old_age.ex │ ├── util │ └── config.ex │ ├── watchers.ex │ └── world │ ├── generator.ex │ ├── location.ex │ ├── pathway.ex │ ├── supervisor.ex │ └── world.ex ├── mix.exs ├── mix.lock ├── rel └── config.exs ├── test ├── test_helper.exs └── test_test.exs └── world /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | 19 | # Log files 20 | var/log/* 21 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 21.2.3 2 | elixir 1.8.0 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.4.2 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Aldric Giacomoni 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dwarlixir 2 | * Travis ![Travis build](https://travis-ci.org/Trevoke/dwarlixir.svg?branch=master) 3 | * Semaphore [![Build Status](https://semaphoreci.com/api/v1/trevoke/dwarlixir/branches/master/badge.svg)](https://semaphoreci.com/trevoke/dwarlixir) 4 | 5 | ## Getting started 6 | 7 | Make sure you have Elixir ~> 1.5.1 available. 8 | 9 | ## README-driven development 10 | 11 | Things I need to bring back in as I switch to ECS: 12 | 13 | - generating a world 14 | - creating location entities 15 | - generating mobs at startup 16 | - putting mobs in locations 17 | - reproduction 18 | 19 | ### components 20 | 21 | - Age 22 | - Mortal 23 | - SexualReproduction (Biology? Subcomponent? Not a component at all?) 24 | - OviparousReproduction 25 | - AIComponent.V1 26 | 27 | ### Thoughts 28 | 29 | What about aging; could be a separate tick for everyone (easy) or some kind of "everything ages at the same time" tick. If the latter, I need, I think, to make it possible to subscribe to _events_ in the ECS framework. 30 | Well, my "Watcher" idea is growing old quickly isn't it. 31 | Unless I create a Universe entity with a Tick component which could get updated with a monotonically increasing value, and that would be something that various things could watch... 32 | 33 | ## A player character: 34 | - can move 35 | - gains "idle xp" ? 36 | - can talk 37 | - can disconnect 38 | 39 | Priority 1: on the server, able to send and receive messages 40 | Priority 2: actually having a working character 41 | Priority 3: being in a room 42 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | config :ecstatic, :watchers, fn() -> Dwarlixir.Watchers.watchers end 6 | 7 | config :logger, 8 | handle_otp_reports: true, 9 | handle_sasl_reports: true 10 | 11 | config :logger, level: :warn 12 | 13 | config :logger, 14 | backends: [{LoggerFileBackend, :error_log}] 15 | 16 | config :logger, :error_log, 17 | path: "#{Path.expand(".")}/var/log/error.log" 18 | 19 | config :sasl, 20 | sasl_error_logger: {:file, 'var/log/sasl_errors.log'}, 21 | error_logger_mf_dir: 'var/log/', 22 | error_logger_mf_maxbytes: 1000, 23 | error_logger_mf_maxfiles: 10 24 | 25 | 26 | # Two options: :short and :long 27 | #config :mobs, lifespan: :short 28 | 29 | # Whether to have mobs auto-spawned when this app starts up 30 | #config :mobs, spawn_on_start: true 31 | 32 | #config :life, start_heartbeat: true 33 | 34 | config :elixir, ansi_enabled: true 35 | 36 | import_config "#{Mix.env}.exs" 37 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :dwarlixir, :world, init: "cdaf62050c854082affa4fdde187428c" 4 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :dwarlixir, :world, init: :new 4 | 5 | config :logger, handle_sasl_reports: false 6 | config :logger, level: :warn 7 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :dwarlixir, :world, init: false 4 | #config :mobs, spawn_on_start: false 5 | #config :life, start_heartbeat: false 6 | -------------------------------------------------------------------------------- /lib/dwarlixir.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir do 2 | 3 | end 4 | -------------------------------------------------------------------------------- /lib/dwarlixir/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Application do 2 | @moduledoc false 3 | 4 | alias Dwarlixir.{World, Mobs, Life, Item, Ecosystem, Connections} 5 | 6 | use Application 7 | 8 | def start(_type, _args) do 9 | import Supervisor.Spec, warn: false 10 | 11 | children = [ 12 | Ecstatic.Supervisor, 13 | {Registry, keys: :unique, name: Registry.HumanControllers}, 14 | {Registry, keys: :unique, name: Registry.Controllers}, 15 | # You know what, the world supervisor needs to do 16 | # all this, and I need a locationsupervisor 17 | # that is a dynamic supervisor 18 | {Registry, keys: :unique, name: World.LocationRegistry}, 19 | {Registry, keys: :unique, name: World.PathwayRegistry}, 20 | {Registry, keys: :duplicate, name: World.Registry}, 21 | World.Supervisor, 22 | {World, %{init: Utils.Config.get(:dwarlixir, :world)[:init]}}, 23 | 24 | {Registry, keys: :unique, name: Mobs.Registry}, 25 | 26 | {Connections.Tcp, 4040}, 27 | 28 | #supervisor(Mobs.Supervisor, [], restart: :permanent), 29 | #worker(Mobs, [%{spawn_on_start: Utils.Config.get(:mobs, :spawn_on_start)}], restart: :permanent), 30 | 31 | #worker(Life.Reaper, [], restart: :permanent), 32 | #supervisor(Registry, [:duplicate, Registry.Tick], id: :tick), 33 | #worker(Life.Timers, [%{start_heartbeat: Utils.Config.get(:life, :start_heartbeat)}], restart: :permanent), 34 | 35 | #supervisor(Registry, [:unique, Registry.Items], id: :items), 36 | 37 | #worker(Ecosystem, [%{}], restart: :permanent), 38 | 39 | ] 40 | 41 | opts = [strategy: :one_for_one, name: Dwarlixir.Supervisor] 42 | Supervisor.start_link(children, opts) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/dwarlixir/components/age.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Components.Age do 2 | use Ecstatic.Component 3 | @default_value %{age: 1, life_expectancy: 80} 4 | end 5 | -------------------------------------------------------------------------------- /lib/dwarlixir/components/container.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Components.Container do 2 | use Ecstatic.Component 3 | end 4 | -------------------------------------------------------------------------------- /lib/dwarlixir/components/dead.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Components.Dead do 2 | use Ecstatic.Component 3 | end 4 | -------------------------------------------------------------------------------- /lib/dwarlixir/components/mortal.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Components.Mortal do 2 | use Ecstatic.Component 3 | @default_value %{mortal: true} 4 | end 5 | -------------------------------------------------------------------------------- /lib/dwarlixir/connections/ranch_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Connections.RanchHandler do 2 | 3 | alias Dwarlixir.{World, Controllers.Human} 4 | 5 | def start_link(ref, socket, transport, opts) do 6 | pid = spawn_link(__MODULE__, :init, [ref, socket, transport, opts]) 7 | {:ok, pid} 8 | end 9 | 10 | def init(ref, socket, transport, _opts = []) do 11 | :ok = :ranch.accept_ack(ref) 12 | loop(socket, transport) 13 | end 14 | 15 | def loop(socket, transport) do 16 | write_line(transport, socket, "Choose a username: ") 17 | {:ok, username} = read_line(transport, socket) 18 | case Human.log_in(username, "password", transport, socket) do 19 | {:ok, user_id} -> 20 | Human.handle(user_id, {:input, "help"}) 21 | loop_connection(transport, socket, user_id) 22 | {:error, :username_taken} -> 23 | write_line(transport, socket, "Username already online. Try another.\n") 24 | loop(socket, transport) 25 | {:error, error} -> write_line(transport, socket, error) 26 | _ -> 27 | :ok = transport.close(socket) 28 | end 29 | # TODO disconnect on bad login 30 | # TODO graceful exit 31 | # TODO graceful error handling? 32 | end 33 | 34 | def loop_connection(transport, socket, user_id) do 35 | case read_line(transport, socket) do 36 | {:ok, input} -> 37 | Human.handle(user_id, {:input, input}) 38 | loop_connection(transport, socket, user_id) 39 | {:error, :closed} -> nil 40 | end 41 | end 42 | 43 | def read_line(transport, socket) do 44 | transport.recv(socket, 0, 5000) 45 | #:gen_tcp.recv(socket, 0) 46 | end 47 | 48 | def write_line(transport, socket, line) do 49 | transport.send(socket, line) 50 | #:gen_tcp.send(socket, line) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/dwarlixir/connections/tcp.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Connections.Tcp do 2 | alias Dwarlixir.Connections, as: DConn 3 | 4 | def child_spec(opts) do 5 | %{ 6 | id: __MODULE__, 7 | start: {__MODULE__, :start_link, [opts]}, 8 | type: :worker, 9 | restart: :permanent, 10 | shutdown: 500 11 | } 12 | end 13 | 14 | def start_link(port) do 15 | opts = [port: port] 16 | {:ok, _} = 17 | :ranch.start_listener( 18 | :dwarlixir_tcp, 19 | 100, 20 | :ranch_tcp, 21 | opts, 22 | DConn.RanchHandler, 23 | [] 24 | ) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/dwarlixir/controllers/generic_mob_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Controllers.Mob do 2 | alias Dwarlixir.{Life, World, Mobs} 3 | # state %{module, id} 4 | 5 | use GenServer 6 | 7 | # @tick 2000 8 | 9 | def start_link(args \\ %{}) do 10 | GenServer.start_link(__MODULE__, args) 11 | end 12 | 13 | def init(state) do 14 | Registry.register(Registry.Tick, :subject_to_time, self()) 15 | {:ok, state} 16 | end 17 | 18 | def handle_cast(:tick, state) do 19 | mob_state = Agent.get(state.agent_pid, &(&1)) 20 | new_mob_state = tick(state, mob_state) 21 | Agent.update(state.agent_pid, fn(_x) -> new_mob_state end) 22 | {:noreply, state} 23 | end 24 | 25 | def tick(state, %{lifespan: 0} = mob_state) do 26 | Agent.stop(state.agent_pid) 27 | Life.Reaper.claim({state.module, state.id}, mob_state.location_id, mob_state) 28 | # TODO this needs to be a more elegant "queue message to everyone in the room that the mob died" 29 | World.Location.announce_death(mob_state.location_id, {{state.module, state.id}, mob_state}) 30 | Registry.unregister(Mobs.Registry, {state.module, state.id}) 31 | GenServer.stop(self()) 32 | Kernel.apply(state.module, :stop, [state.id]) 33 | end 34 | 35 | def tick(state, %{pregnant: true, ticks_to_birth: 1} = mob_state) do 36 | Kernel.apply(state.module, :new_life, [%{location_id: mob_state.location_id}]) 37 | #TODO add event 38 | %{mob_state | pregnant: false, ticks_to_birth: nil} 39 | end 40 | 41 | def tick(state, %{pregnant: true} = mob_state) do 42 | new_mob_state = let_lady_luck_decide(state, mob_state) 43 | %{new_mob_state | lifespan: new_mob_state.lifespan - 1, ticks_to_birth: mob_state.ticks_to_birth - 1} 44 | end 45 | 46 | def tick(state, mob_state) do 47 | new_mob_state = let_lady_luck_decide(state, mob_state) 48 | %{new_mob_state | lifespan: new_mob_state.lifespan - 1} 49 | end 50 | 51 | def let_lady_luck_decide(state, mob_state) do 52 | case Enum.random(1..10000) do 53 | x when x < 9000 -> mob_state 54 | x when x < 9950 -> Kernel.apply(state.module, :move_to_random_location, [mob_state]) 55 | x when x <= 10000 -> Kernel.apply(state.module, :try_to_mate, [mob_state]) 56 | _ -> mob_state 57 | end 58 | end 59 | 60 | # Tick when lifespan is 1 -- take all code from `terminate in mob template?` 61 | # Tick.. When pregnant 62 | 63 | def terminate(reason, _state) do 64 | Registry.unregister(Registry.Tick, :subject_to_time) 65 | #Registry.unregister(Controllers.Registry, via_tuple(state.id)) 66 | reason 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/dwarlixir/controllers/human/impl.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Controllers.Human.Impl do 2 | alias Dwarlixir.Controllers.Human 3 | alias Human.Server 4 | 5 | def log_in(user_id, _password, transport, socket) do 6 | user_id = String.trim user_id 7 | params = 8 | %Human{ 9 | id: user_id, 10 | socket: socket, 11 | transport: transport, 12 | entity: Dwarlixir.Entities.PlayerCharacter.new 13 | } 14 | case Server.start_link(params) do 15 | {:ok, _pid} -> {:ok, user_id} 16 | {:error, {:already_started, _pid}} -> {:error, :username_taken} 17 | end 18 | 19 | #room = 1 20 | #Human.join_room(user_id, room) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/dwarlixir/controllers/human/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Controllers.Human.Server do 2 | use GenServer 3 | 4 | def start_link(args \\ %Dwarlixir.Controllers.Human{}) do 5 | GenServer.start_link(__MODULE__, args, name: via_tuple(args.id, args.id)) 6 | end 7 | 8 | def init(args) do 9 | Registry.register(Registry.Controllers, "human", args.id) 10 | {:ok, args} 11 | end 12 | 13 | def via_tuple(id, value) do 14 | {:via, Registry, {Registry.HumanControllers, id, value}} 15 | end 16 | 17 | def via_tuple(id) do 18 | {:via, Registry, {Registry.HumanControllers, id}} 19 | end 20 | 21 | def handle(user_id, message) do 22 | GenServer.cast(via_tuple(user_id), message) 23 | end 24 | 25 | def handle_cast({:input, input}, state) do 26 | state.transport.send(state.socket, input) 27 | {:noreply, state} 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/dwarlixir/controllers/human_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Controllers.Human do 2 | alias __MODULE__.{Impl, Server} 3 | 4 | defstruct [ 5 | :transport, 6 | :socket, 7 | :id, 8 | :entity, 9 | :location_id, 10 | exits: [], 11 | messages: [] 12 | ] 13 | 14 | def log_in(user_id, password, transport, socket) do 15 | Impl.log_in(user_id, password, transport, socket) 16 | end 17 | 18 | def handle(user_id, message) do 19 | Server.handle(user_id, message) 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /lib/dwarlixir/controllers/human_old.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Controllers.HumanOld do 2 | alias Dwarlixir.Controllers 3 | alias Dwarlixir.World 4 | defstruct [ 5 | :socket, :id, :location_id, exits: [], messages: [] 6 | ] 7 | use GenServer 8 | 9 | def start_link(args \\ %__MODULE__{}) do 10 | GenServer.start_link(__MODULE__, args, name: via_tuple(args.id)) 11 | end 12 | 13 | def init(args) do 14 | Registry.update_value(Registry.HumanControllers, args.id, fn(_x) -> args.id end) 15 | Registry.register(Registry.Controllers, "human", args.id) 16 | Registry.register(Registry.Tick, :subject_to_time, self()) 17 | {:ok, args} 18 | end 19 | 20 | def via_tuple(id) do 21 | {:via, Registry, {Registry.HumanControllers, id}} 22 | end 23 | 24 | def log_in(user_id, _password, socket) do 25 | user_id = String.trim user_id 26 | case Controllers.Human.start_link(%__MODULE__{id: user_id, socket: socket}) do 27 | {:ok, _pid} -> {:ok, user_id} 28 | {:error, {:already_started, _pid}} -> {:error, :username_taken} 29 | end 30 | end 31 | 32 | def join_room(user_id, loc_id)do 33 | GenServer.cast(via_tuple(user_id), {:join_room, loc_id}) 34 | end 35 | 36 | def terminate(reason, _state) do 37 | #Registry.unregister(Registry.HumanControllers, state.id) 38 | reason 39 | end 40 | 41 | defp polish_event(string, :arrive, from), do: string <> " arrived from #{from}.\n" 42 | defp polish_event(string, :depart, to), do: string <> " is leaving going #{to}.\n" 43 | defp polish_event(string, :death, nil), do: string <> " died.\n" 44 | 45 | def handle(user_id, {:input, input}) do 46 | GenServer.cast(via_tuple(user_id), {:input, String.trim(input)}) 47 | end 48 | 49 | def handle(user_id, message) do 50 | GenServer.cast(via_tuple(user_id), message) 51 | end 52 | 53 | # messages => [{:arrive, mob_id, loc}, {:depart}] 54 | # => %{:arrive => [{}], :depart => [{}]} 55 | # => %{:arrive => ["John McKoala", "Oliver McKoala"]} 56 | # => [["John McKoala, OliverMcKoala arrive."]] 57 | # => "Foo\nbar" 58 | 59 | def handle_cast(:tick, state) do 60 | events = state.messages 61 | |> Enum.reduce(%{}, fn(msg, acc) -> 62 | Map.update(acc, {elem(msg, 0), elem(msg, 2)}, [msg], fn(v) -> [msg | v] end) 63 | end) 64 | |> Enum.sort 65 | |> Enum.map(fn({{event_name, event_property}, instances}) -> 66 | Enum.map(instances, fn(instance) -> elem(instance, 1).name end) 67 | |> Enum.join(", ") 68 | |> polish_event(event_name, event_property) 69 | end) 70 | |> Enum.join 71 | 72 | write_line(state.socket, events) 73 | 74 | # handle arrive messages - write_line(state.socket, "#{info.name} arrived from #{from_loc}.\n") 75 | # handle depart messages - write_line(state.socket, "#{info.name} is leaving towards #{to}.\n") 76 | # handle death messages - write_line(state.socket, "#{info.name} has just died.") 77 | {:noreply, %__MODULE__{state | messages: []}} 78 | end 79 | 80 | def handle_cast({:arrive, _info, _from_loc} = message, state) do 81 | {:noreply, %__MODULE__{state | messages: [message | state.messages]}} 82 | end 83 | 84 | def handle_cast({:depart, _info, _to} = message, state) do 85 | {:noreply, %__MODULE__{state | messages: [message | state.messages]}} 86 | end 87 | 88 | def handle_cast({:death, _info} = message, state) do 89 | {:noreply, %__MODULE__{state | messages: [Tuple.append(message, nil) | state.messages]}} 90 | end 91 | 92 | def handle_cast({:join_room, loc_id}, state) do 93 | {:ok, exits} = World.Location.arrive(loc_id, 94 | { 95 | {__MODULE__, state.id}, 96 | public_info(state), 97 | "seemingly nowhere"}) 98 | 99 | { 100 | :noreply, 101 | %__MODULE__{state | 102 | location_id: loc_id, 103 | exits: exits}} 104 | end 105 | 106 | def handle_cast({:input, "help"}, state) do 107 | table = TableRex.quick_render!([ 108 | ["look", "see what is in the room"], 109 | ["wall ", "talk to all other users"], 110 | ["", "move"], 111 | ["who", "see who is logged in"], 112 | ["help", "read this again"], 113 | ["quit", "log out"], 114 | ["spawn_more", "spawn more mobs"] 115 | ], ["Command", "Description"]) 116 | write_line(state.socket, Bunt.ANSI.format [ 117 | :bright, 118 | :blue, 119 | """ 120 | Welcome, #{state.id}! Here are the available commands. 121 | #{table} 122 | """ 123 | ] 124 | ) 125 | {:noreply, state} 126 | end 127 | 128 | def handle_cast({:input, "who"}, state) do 129 | users = 130 | Registry.Controllers 131 | |> Registry.match("human", :_) 132 | |> Enum.map(&([elem(&1, 1)])) 133 | output = TableRex.quick_render!(users, ["Users logged in"]) <> "\n" 134 | write_line(state.socket, output) 135 | {:noreply, state} 136 | end 137 | 138 | def handle_cast({:input, "spawn_more"}, state) do 139 | write_line(state.socket, "Spawning 40 more mobs.\n") 140 | Mobs.create_mobs(40) 141 | {:noreply, state} 142 | end 143 | 144 | def handle_cast({:input, "quit"}, state) do 145 | write_line(state.socket, "Goodbye.\n") 146 | World.Location.depart( 147 | state.location_id, 148 | { 149 | {__MODULE__, state.id}, 150 | state, 151 | "the real world" 152 | } 153 | ) 154 | :gen_tcp.close(state.socket) 155 | {:stop, :normal, state} 156 | end 157 | 158 | def handle_cast({:input, "look"}, state) do 159 | things_seen = World.Location.look(state.location_id) 160 | 161 | text = """ 162 | #{things_seen.description} 163 | #{Bunt.ANSI.format [:green, read_exits(things_seen.exits)]} 164 | #{read_entities(things_seen.living_things)} 165 | #{read_entities(things_seen.items)} 166 | """ 167 | |> String.trim() 168 | state.socket 169 | |> write_line(text <> "\n") 170 | {:noreply, state} 171 | end 172 | 173 | def handle_cast({:input, "wall " <> message}, state) do 174 | Registry.Controllers 175 | |> Registry.match("human", :_) 176 | |> Enum.map(&(elem(&1, 0))) 177 | |> Enum.each(fn(x) -> GenServer.cast(x, {:receive_wall, state.id, message}) end) 178 | {:noreply, state} 179 | end 180 | 181 | def handle_cast({:receive_wall, from_user, message}, state) do 182 | write_line(state.socket, Bunt.ANSI.format [:bright, :yellow, "#{from_user} says: #{message}\n"]) 183 | {:noreply, state} 184 | end 185 | 186 | def handle_cast({:input, input}, state) do 187 | cond do 188 | pathway = Enum.find(state.exits, &(&1.name == input)) -> 189 | with info <- public_info(state), 190 | :ok <- World.Location.depart(state.location_id, {{__MODULE__, state.id}, info, pathway.from_id}), 191 | {:ok, exits} <- World.Location.arrive(pathway.from_id, {{__MODULE__, state.id}, info, state.location_id}) do 192 | GenServer.cast(self(), {:input, "look"}) 193 | {:noreply, %__MODULE__{state | location_id: pathway.from_id, exits: exits}} 194 | end 195 | true -> 196 | write_line(state.socket, "Sorry, I don't understand that.") 197 | {:noreply, state} 198 | end 199 | end 200 | 201 | defp read_entities(entities) do 202 | entities 203 | |> Enum.group_by(&(&1)) 204 | |> Enum.map(fn({k, v}) -> {k, Enum.count(v)} end) 205 | |> Enum.sort(fn({_n1, c1}, {_n2, c2}) -> c1 > c2 end) 206 | |> Enum.map(fn 207 | {name, 1} -> name 208 | {name, count} -> "#{count} #{name}" 209 | end) 210 | |> Enum.join(", ") 211 | end 212 | 213 | defp read_exits(exits) do 214 | exit_text = 215 | exits 216 | |> Enum.map(fn(x) -> x.name end) 217 | |> Enum.join(", ") 218 | "Exits: #{exit_text}." 219 | end 220 | 221 | defp write_line(socket, line) do 222 | :gen_tcp.send(socket, line) 223 | end 224 | 225 | defp public_info(state) do 226 | %{ 227 | gender: :male, 228 | name: state.id 229 | } 230 | end 231 | 232 | end 233 | -------------------------------------------------------------------------------- /lib/dwarlixir/ecosystem/ecosystem.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Ecosystem do 2 | alias Dwarlixir.Mobs 3 | @moduledoc """ 4 | Documentation for Ecosystem. 5 | """ 6 | 7 | use GenServer 8 | 9 | def start_link(%{} = args) do 10 | GenServer.start_link(__MODULE__, args, name: __MODULE__) 11 | end 12 | 13 | def init(%{} = state) do 14 | {:ok, tref} = Petick.start( 15 | interval: 300000, callback: {__MODULE__, :check_system} 16 | ) 17 | state = Map.put(state, :tref, tref) 18 | {:ok, state} 19 | end 20 | 21 | def free_percentage_of_memory(mem_data_list) do 22 | total = mem_data_list[:total_memory] 23 | free = mem_data_list[:free_memory] 24 | free / total * 100 25 | end 26 | 27 | def check_system(_pid) do 28 | GenServer.cast(__MODULE__, :check_system) 29 | end 30 | 31 | def handle_cast(:check_system, state) do 32 | mem_data_list = :memsup.get_system_memory_data 33 | ecosystem_sanity(free_percentage_of_memory(mem_data_list)) 34 | {:noreply, state} 35 | end 36 | 37 | def ecosystem_sanity(percentage) when percentage < 10 do 38 | Mobs.deny_births 39 | end 40 | 41 | # TODO if the percentage is never more than 30, oops. 42 | def ecosystem_sanity(percentage) when percentage > 30 do 43 | Mobs.allow_births 44 | end 45 | 46 | def ecosystem_sanity(_percentage), do: nil 47 | 48 | end 49 | -------------------------------------------------------------------------------- /lib/dwarlixir/entities/bird.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Entities.Bird do 2 | use Ecstatic.Entity 3 | 4 | alias Dwarlixir.Components, as: C 5 | 6 | @default_components [C.Age, C.Mortal] 7 | 8 | 9 | # TODO reproduction will involve laying eggs 10 | 11 | 12 | #defp random_lifespan(:short), do: 300 + Enum.random(1..200) 13 | #defp random_lifespan(_args), do: 1800 + Enum.random(1..7200) 14 | 15 | end 16 | -------------------------------------------------------------------------------- /lib/dwarlixir/entities/dwarf.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Entities.Dwarf do 2 | use Ecstatic.Entity 3 | 4 | alias Dwarlixir.Components, as: C 5 | 6 | @default_components [C.Age, C.Mortal] 7 | 8 | #defp random_lifespan(:short), do: 300 + Enum.random(1..200) 9 | #defp random_lifespan(_args), do: 1800 + Enum.random(1..7200) 10 | end 11 | -------------------------------------------------------------------------------- /lib/dwarlixir/entities/location.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Entities.Location do 2 | use Ecstatic.Entity 3 | @default_components [Container] 4 | end 5 | -------------------------------------------------------------------------------- /lib/dwarlixir/entities/player_character.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Entities.PlayerCharacter do 2 | use Ecstatic.Entity 3 | 4 | alias Dwarlixir.Components, as: C 5 | 6 | @default_components [C.Mortal] 7 | 8 | #defp random_lifespan(:short), do: 300 + Enum.random(1..200) 9 | #defp random_lifespan(_args), do: 1800 + Enum.random(1..7200) 10 | end 11 | -------------------------------------------------------------------------------- /lib/dwarlixir/item/corpse.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Item.Corpse do 2 | alias Dwarlixir.World 3 | alias Dwarlixir.Item 4 | use GenServer 5 | 6 | def start_link(args) do 7 | GenServer.start_link(__MODULE__, args, name: via_tuple(args.id)) 8 | end 9 | 10 | def via_tuple(id), do: {:via, Registry, {Registry.Items, id}} 11 | 12 | def init(state) do 13 | Registry.register(Registry.Tick, :subject_to_time, nil) 14 | World.Location.place_item(state.location_id, {__MODULE__, state.id}, state) 15 | {:ok, Map.put(state, :lifespan, 100)} 16 | end 17 | 18 | def handle_cast(:tick, %{lifespan: 0} = state) do 19 | # TODO send a message? 20 | GenServer.stop(self()) 21 | {:noreply, state} 22 | end 23 | 24 | def handle_cast(:tick, %{lifespan: lifespan} = state) do 25 | {:noreply, %{state | lifespan: lifespan - 1}} 26 | end 27 | 28 | def terminate(reason, state) do 29 | World.Location.remove_item(state.location_id, {__MODULE__, state.id}) 30 | Registry.unregister(Registry.Tick, self()) 31 | Registry.unregister(Registry.Items, state.id) 32 | reason 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/dwarlixir/item/egg.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Item.Egg do 2 | alias Dwarlixir.Item 3 | alias Dwarlixir.World 4 | 5 | use GenServer 6 | 7 | def via_tuple(id), do: {:via, Registry, {Registry.Items, id}} 8 | 9 | def start_link(args) do 10 | GenServer.start_link(__MODULE__, args, name: via_tuple(args.id)) 11 | end 12 | 13 | def init(state) do 14 | state = 15 | state 16 | |> Map.put(:name, "egg") 17 | |> Map.put(:lifespan, 30) 18 | Registry.register(Registry.Tick, :subject_to_time, nil) 19 | World.Location.place_item(state.location_id, {__MODULE__, state.id}, state) 20 | {:ok, state} 21 | end 22 | 23 | def handle_cast(:tick, %{lifespan: 0} = state) do 24 | World.Location.remove_item(state.location_id, {__MODULE__, state.id}) 25 | birth = 26 | Task.async( 27 | state.module, 28 | :birth, 29 | [state] 30 | ) 31 | 32 | Task.yield(birth, 50) || Task.shutdown(birth) 33 | 34 | # TODO send a message? 35 | {:stop, :normal, state} 36 | end 37 | 38 | def handle_cast(:tick, %{lifespan: lifespan} = state) do 39 | {:noreply, %{state | lifespan: lifespan - 1}} 40 | end 41 | 42 | def terminate(reason, state) do 43 | Registry.unregister(Registry.Tick, self()) 44 | Registry.unregister(Registry.Items, state.id) 45 | reason 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/dwarlixir/item/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Item.Supervisor do 2 | alias Dwarlixir.Item 3 | 4 | use Supervisor 5 | 6 | def start_link(args \\ []) do 7 | Supervisor.start_link(__MODULE__, args, name: __MODULE__) 8 | end 9 | 10 | def init(_args) do 11 | children = [] 12 | supervise(children, strategy: :one_for_one) 13 | end 14 | 15 | def create(:corpse, loc_id, public_info) do 16 | corpse = worker( 17 | Item.Corpse, 18 | [Map.put(public_info, :location_id, loc_id)], 19 | [id: public_info.id, restart: :transient] 20 | ) 21 | {:ok, _pid} = Supervisor.start_child(__MODULE__, corpse) 22 | end 23 | 24 | def create(:egg, loc_id, public_info) do 25 | egg_id = UUID.uuid4(:hex) 26 | egg = worker( 27 | Item.Egg, 28 | [Map.merge(public_info, %{location_id: loc_id, id: egg_id})], 29 | [id: egg_id, restart: :transient] 30 | ) 31 | {:ok, pid} = Supervisor.start_child(__MODULE__, egg) 32 | {:ok, pid} 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/dwarlixir/life/reaper.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Life.Reaper do 2 | alias Dwarlixir.Item 3 | use GenServer 4 | 5 | def start_link() do 6 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 7 | end 8 | 9 | def init(:ok) do 10 | {:ok, %{}} 11 | end 12 | 13 | def claim(mob_id, loc_id, public_info) do 14 | GenServer.cast(__MODULE__, {:claim, mob_id, loc_id, public_info}) 15 | end 16 | 17 | def handle_cast({:claim, {module, _mob_id}, loc_id, public_info}, state) do 18 | corpse_id = UUID.uuid4(:hex) 19 | corpse_state = Map.put(public_info, :id, corpse_id) 20 | Item.Supervisor.create(:corpse, loc_id, corpse_state) 21 | {:noreply, state} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/dwarlixir/life/timers.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Life.Timers do 2 | use GenServer 3 | 4 | def start_link(%{} = opts) do 5 | GenServer.start_link(__MODULE__, opts, name: :life_timers) 6 | end 7 | 8 | def init(%{start_heartbeat: true}), do: {:ok, %{heartbeat: new_timer()}} 9 | def init(args), do: {:ok, args} 10 | 11 | def start_heartbeat do 12 | GenServer.call(:life_timers, :start_heartbeat) 13 | end 14 | 15 | def stop_heartbeat do 16 | GenServer.call(:life_timers, :stop_heartbeat) 17 | end 18 | 19 | def handle_call(:start_heartbeat, _from, state) do 20 | tref = new_timer() 21 | {:reply, tref, Map.put(state, :heartbeat, tref)} 22 | end 23 | 24 | def handle_call(:stop_heartbeat, _from, %{heartbeat: heartbeat} = state) do 25 | :ok = Petick.terminate(heartbeat) 26 | {:reply, :ok, Map.delete(state, :heartbeat)} 27 | end 28 | 29 | def handle_call(:stop_heartbeat, _from, state), do: {:reply, :no_heartbeat, state} 30 | 31 | defp new_timer do 32 | {:ok, tref} = 33 | Petick.start( 34 | interval: 1000, 35 | callback: {__MODULE__, :send_heartbeat}) 36 | tref 37 | end 38 | 39 | def send_heartbeat(_calling_pid) do 40 | Registry.dispatch(Registry.Tick, :subject_to_time, fn entries -> 41 | for {proc_ref, _} <- entries, do: GenServer.cast(proc_ref, :tick) 42 | end) 43 | end 44 | 45 | 46 | end 47 | -------------------------------------------------------------------------------- /lib/dwarlixir/mobs/mob_template.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Mobs.MobTemplate do 2 | alias Dwarlixir.Mobs 3 | 4 | defmacro __using__(_) do 5 | quote do 6 | defstruct [ 7 | :id, :location_id, :lifespan, :gender, :controller, :pregnant, 8 | :ticks_to_birth, name: "", exits: [] 9 | ] 10 | use GenServer 11 | 12 | def start_link(args) do 13 | GenServer.start(__MODULE__, args, name: via_tuple(args.id), restart: :transient) 14 | end 15 | 16 | def via_tuple(id), do: {:via, Registry, {Mobs.Registry, id}} 17 | 18 | def init(%__MODULE__{location_id: location_id} = state) do 19 | {:ok, exits} = World.Location.arrive(location_id, {{__MODULE__, state.id}, public_info(state), "seemingly nowhere"}) 20 | new_state = %{state | exits: exits} 21 | # TODO remove timer ref? 22 | {:ok, apid} = Agent.start_link(fn() -> new_state end) 23 | {:ok, cpid} = Controllers.Mob.start_link(%{module: __MODULE__, id: new_state.id, agent_pid: apid}) 24 | {:ok, %{agent_pid: apid, controller_pid: cpid, id: state.id}} 25 | end 26 | 27 | def set_location(mob_id, loc_id, exits), do: GenServer.cast(via_tuple(mob_id), {:set_location, loc_id, exits}) 28 | def handle_cast({:set_location, loc_id, exits}, state) do 29 | Agent.update(state.agent_pid, fn(x) -> Map.merge(x, %{location_id: loc_id, exits: exits}) end) 30 | {:noreply, state} 31 | end 32 | 33 | def handle_cast({:arrive, info, from_loc}, state) do 34 | {:noreply, state} 35 | end 36 | 37 | def handle_cast({:depart, info, to_loc}, state) do 38 | {:noreply, state} 39 | end 40 | 41 | # spec: state :: state 42 | # TODO more like reproduction, return state and list of messages? 43 | def move_to_random_location(%{location_id: loc_id, id: id, exits: exits} = state) do 44 | with true <- Enum.any?(exits), 45 | info <- public_info(state), 46 | %{from_id: new_loc_id} <- Enum.random(exits), 47 | :ok <- World.Location.depart(loc_id, {{__MODULE__, id}, info, new_loc_id}), 48 | {:ok, new_exits} <- World.Location.arrive(new_loc_id, {{__MODULE__, id}, info, loc_id}) do 49 | %{state | location_id: new_loc_id, exits: new_exits} 50 | else 51 | false -> state 52 | :not_in_location -> state 53 | end 54 | end 55 | 56 | def try_to_mate(%{pregnant: true} = state), do: state 57 | # spec: state :: state 58 | # TODO return list of messages out of here... ? 59 | def try_to_mate(state) do 60 | looking_for = case state.gender do 61 | :male -> :female 62 | :female -> :male 63 | end 64 | 65 | possible_partners_task = 66 | Task.async(World.Location, :mobs, [state.location_id]) 67 | 68 | case Task.yield(possible_partners_task, 50) || Task.shutdown(possible_partners_task) do 69 | nil -> state 70 | {:ok, possible_partners} -> 71 | {:ok, {new_state, messages}} = Mobs.SexualReproduction.call({state, []}, {state.gender, looking_for, __MODULE__, possible_partners}) 72 | # TODO Task for this 73 | Enum.each(messages, fn({m, f, arglist}) -> Kernel.apply(m, f, arglist) end) 74 | new_state 75 | end 76 | end 77 | 78 | def depregnantize(id), do: GenServer.cast(via_tuple(id), :depregnantize) 79 | def handle_cast(:depregnantize, state) do 80 | Agent.update(state.agent_pid, fn(x) -> Map.merge(x, %{pregnant: false, ticks_to_birth: nil}) end) 81 | {:noreply, state} 82 | end 83 | 84 | # This has made so many people laugh that I can't rename it. 85 | def pregnantize(mob_id) do 86 | GenServer.cast(via_tuple(mob_id), :pregnantize) 87 | end 88 | 89 | def handle_cast(:pregnantize, state) do 90 | Agent.update(state.agent_pid, fn(x) -> Map.merge(x, %{pregnant: true, ticks_to_birth: 100}) end) 91 | {:noreply, state} 92 | end 93 | 94 | def handle(id, message), do: GenServer.cast(via_tuple(id), message) 95 | 96 | # Yeah so this should actually *do* something 97 | # But for now it'll help avoid mailboxes getting full. 98 | def handle_cast(_msg, state), do: {:noreply, state} 99 | 100 | def stop(mob_id) do 101 | GenServer.stop(via_tuple(mob_id)) 102 | end 103 | 104 | defp public_info(state) do 105 | %{ 106 | gender: state.gender, 107 | name: state.name, 108 | pregnant: state.pregnant 109 | } 110 | end 111 | 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/dwarlixir/mobs/sexual_reproduction.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Mobs.SexualReproduction do 2 | alias Dwarlixir.Mobs 3 | # entities_around is of the form %{{module, id} => public_info} 4 | def call({state, messages}, {:male, :female, my_species, entities_around}) do 5 | possible_females = 6 | Enum.filter( 7 | entities_around, 8 | fn({{their_species, _}, their_stats}) -> 9 | their_species == my_species && 10 | their_stats.gender == :female && 11 | their_stats.pregnant == false 12 | end 13 | ) 14 | 15 | if Enum.empty? possible_females do 16 | {:ok, {state, messages}} 17 | else 18 | # TODO genetics 19 | {{module, id}, _info} = 20 | possible_females 21 | |> Enum.random 22 | {:ok, {state, [{module, :pregnantize, [id]} | messages]}} 23 | end 24 | end 25 | 26 | def call({%{pregnant: true} = state, messages}, {:female, _, _, _}) do 27 | {:ok, {state, messages}} 28 | end 29 | 30 | def call({state, messages}, {:female, :male, my_species, entities_around}) do 31 | possible_males = 32 | Enum.filter( 33 | entities_around, 34 | fn({{their_species, _}, their_stats}) -> 35 | their_species == my_species && their_stats.gender == :male 36 | end 37 | ) 38 | 39 | if Enum.empty? possible_males do 40 | {:ok, {state, messages}} 41 | else 42 | # TODO : genetics 43 | # I don't really care about the male right now, but eventually I'll care 44 | # about what he brings to the equation. 45 | {{_module, _id}, _info} = 46 | possible_males 47 | |> Enum.random 48 | {:ok, {state, [{my_species, :pregnantize, [state.id]} | messages]}} 49 | end 50 | end 51 | 52 | def call({state, messages}, _args), do: {state, messages} 53 | 54 | end 55 | -------------------------------------------------------------------------------- /lib/dwarlixir/systems/aging.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Systems.Aging do 2 | use Ecstatic.System 3 | 4 | alias Dwarlixir.Components, as: C 5 | 6 | def aspect, do: %Ecstatic.Aspect{with: [C.Age]} 7 | 8 | def dispatch(entity) do 9 | age = Entity.find_component(entity, C.Age) 10 | %Ecstatic.Changes{updated: [%{age | state: %{age.state | age: age.state.age + 1}}]} 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/dwarlixir/systems/death.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Systems.Death do 2 | use Ecstatic.System 3 | 4 | def aspect, do: %Ecstatic.Aspect{with: [Mortal]} 5 | 6 | def dispatch(entity) do 7 | # TODO what happens when something dies, anyway? 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/dwarlixir/systems/old_age.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Systems.OldAge do 2 | use Ecstatic.System 3 | 4 | alias Dwarlixir.Components, as: C 5 | 6 | def aspect, do: %Ecstatic.Aspect{with: [C.Age, C.Mortal]} 7 | 8 | def dispatch(entity) do 9 | %Ecstatic.Changes{attached: [C.Dead], removed: [C.Age]} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/dwarlixir/util/config.ex: -------------------------------------------------------------------------------- 1 | defmodule Utils.Config do 2 | @moduledoc """ 3 | This module handles fetching values from the config with some additional niceties 4 | """ 5 | 6 | @doc """ 7 | Fetches a value from the config, or from the environment if {:system, "VAR"} 8 | is provided. 9 | An optional default value can be provided if desired. 10 | ## Example 11 | iex> {test_var, expected_value} = System.get_env |> Enum.take(1) |> List.first 12 | ...> Application.put_env(:myapp, :test_var, {:system, test_var}) 13 | ...> ^expected_value = #{__MODULE__}.get(:myapp, :test_var) 14 | ...> :ok 15 | :ok 16 | iex> Application.put_env(:myapp, :test_var2, 1) 17 | ...> 1 = #{__MODULE__}.get(:myapp, :test_var2) 18 | 1 19 | iex> :default = #{__MODULE__}.get(:myapp, :missing_var, :default) 20 | :default 21 | """ 22 | @spec get(atom, atom, term | nil) :: term 23 | def get(app, key, default \\ nil) when is_atom(app) and is_atom(key) do 24 | case Application.get_env(app, key) do 25 | {:system, env_var} -> 26 | case System.get_env(env_var) do 27 | nil -> default 28 | val -> val 29 | end 30 | {:system, env_var, preconfigured_default} -> 31 | case System.get_env(env_var) do 32 | nil -> preconfigured_default 33 | val -> val 34 | end 35 | nil -> 36 | default 37 | val -> 38 | val 39 | end 40 | end 41 | 42 | @doc """ 43 | Same as get/3, but returns the result as an integer. 44 | If the value cannot be converted to an integer, the 45 | default is returned instead. 46 | """ 47 | @spec get_integer(atom(), atom(), integer()) :: integer 48 | def get_integer(app, key, default \\ nil) do 49 | case get(app, key, nil) do 50 | nil -> default 51 | n when is_integer(n) -> n 52 | n -> 53 | case Integer.parse(n) do 54 | {i, _} -> i 55 | :error -> default 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/dwarlixir/watchers.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Watchers do 2 | use Ecstatic.Watcher 3 | 4 | alias Dwarlixir.Components, as: C 5 | alias Dwarlixir.Systems, as: S 6 | 7 | watch_component C.Age, run: S.Aging, every: 6_000 8 | watch_component C.Age, 9 | run: S.Dying, 10 | when: fn(_e, c) -> c.state.age > c.state.life_expectancy end 11 | 12 | # watch_component C.Age, :updated, 13 | # fn(_e, post) -> post.state.age > post.state.life_expectancy end, S.OldAgeSystem 14 | end 15 | -------------------------------------------------------------------------------- /lib/dwarlixir/world/generator.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.World.Generator do 2 | alias Dwarlixir.World 3 | @node_count 3000 4 | @edge_count 5000 5 | 6 | def call do 7 | connect_all_islands(Enum.to_list(1..@node_count), make_edge_list()) # [{loc_id, to_id}] 8 | |> Enum.group_by(fn({from, _to}) -> from end) # %{loc_id => [{loc_id, to_id}]} 9 | |> Enum.map(fn({loc, pathways}) -> 10 | World.location("#{loc}", "#{loc}", "room id #{loc}", 11 | Enum.map(Enum.uniq(pathways), fn({_to, from}) -> 12 | World.partial_pathway("#{from}", "#{from}") 13 | end)) 14 | end) 15 | end 16 | 17 | def random_node, do: Enum.random(1..@node_count) 18 | 19 | def edge_pair(a, b) when a != b, do: [{a, b}, {b, a}] 20 | def edge_pair(_a, _b), do: [] 21 | 22 | def make_edge_list() do 23 | 1..@edge_count 24 | |> Enum.flat_map( 25 | fn(_x) -> edge_pair(random_node(), random_node()) 26 | end) 27 | end 28 | 29 | def direct_edges(node1, edge_list) do 30 | Enum.filter(edge_list, fn({a, _b}) -> a == node1 end) 31 | end 32 | 33 | def get_connected(node1, edge_list) do 34 | Enum.uniq traverse(node1, edge_list, []) 35 | end 36 | 37 | def traverse(node1, edge_list, visited) do 38 | if Enum.member?(visited, node1) do 39 | visited 40 | else 41 | x = [node1 | visited] 42 | y = direct_edges(node1, edge_list) 43 | List.foldl(y, x ,fn({_a, b}, acc) -> traverse(b, edge_list, acc) end) 44 | end 45 | end 46 | 47 | def find_islands(nodes, edge_list) do 48 | find_island(nodes, edge_list, []) 49 | end 50 | 51 | def find_island([] = _nodes, _edge_list, islands), do: islands 52 | 53 | def find_island([h | t] = nodes, edge_list, islands) do 54 | connected = get_connected(h, edge_list) 55 | if Enum.empty?(connected) do 56 | find_island(t, edge_list, islands) 57 | else 58 | unconnected = nodes -- connected 59 | islands = [connected | islands] 60 | find_island(unconnected, edge_list, islands) 61 | end 62 | end 63 | 64 | def generate_necessary_bridges(islands), do: generate_necessary_bridges(islands, []) 65 | 66 | def generate_necessary_bridges([_island], acc), do: acc 67 | 68 | def generate_necessary_bridges([h|t] = islands, acc) when length(islands) == 2 do 69 | edge_pair(Enum.random(h), Enum.random(List.first(t))) ++ acc 70 | end 71 | 72 | def generate_necessary_bridges([h|t] = islands, acc) when length(islands) > 2 do 73 | [t1|_t2] = islands 74 | generate_necessary_bridges(t, edge_pair(Enum.random(h), Enum.random(t1)) ++ acc) 75 | end 76 | 77 | def connect_all_islands(nodes, edge_list) do 78 | bridges = 79 | nodes 80 | |> find_islands(edge_list) 81 | |> generate_necessary_bridges(edge_list) 82 | bridges ++ edge_list 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /lib/dwarlixir/world/location.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.World.Location do 2 | alias Dwarlixir.World 3 | defstruct [ 4 | :id, :name, :description, :pathways, items: %{}, 5 | entities: %{} 6 | ] 7 | @type t :: %World.Location { 8 | id: String.t, 9 | name: String.t, 10 | description: String.t, 11 | pathways: [World.Pathway.t], 12 | items: map(), 13 | entities: map() 14 | } 15 | use GenServer 16 | 17 | alias World.{Location, LocationRegistry, Pathway, PathwayRegistry} 18 | 19 | def start_link(%__MODULE__{} = args) do 20 | GenServer.start_link(__MODULE__, args, name: via_tuple(args.id)) 21 | end 22 | 23 | def via_tuple(id) do 24 | {:via, Registry, {LocationRegistry, id}} 25 | end 26 | 27 | def init(%__MODULE__{pathways: pathways, id: id} = state) do 28 | Process.flag(:trap_exit, true) 29 | {id, nil} = Registry.update_value(LocationRegistry, id, fn(_x) -> id end) 30 | Registry.register(World.Registry, "location", id) 31 | pathways = 32 | for %{from_id: from_id, name: name} <- pathways do 33 | %Pathway{from_id: from_id, to_id: id, name: name} 34 | end 35 | launch_known_pathways(id, pathways) 36 | check_for_other_pathways_to_monitor(id) 37 | {:ok, Map.put(state, :pathways, pathways)} 38 | end 39 | 40 | # TODO will I need to create an eye for this? 41 | # Or will this need to be just.. In ets? 42 | def look(loc_id) do 43 | GenServer.call(via_tuple(loc_id), :look) 44 | end 45 | 46 | # TODO to_id - always a good idea? 47 | def depart(current_location, {{module, mob_id}, public_info, to_id}) do 48 | GenServer.call(via_tuple(current_location), {:depart, {{module, mob_id}, public_info, to_id}}) 49 | end 50 | 51 | def announce_death(current_location, {{module, mob_id}, public_info}) do 52 | GenServer.cast(via_tuple(current_location), {:announce_death, {{module, mob_id}, public_info}}) 53 | end 54 | 55 | # TODO from can also be from birth, I think? 56 | def arrive(new_location, {{module, id}, public_info, from}) do 57 | GenServer.call(via_tuple(new_location), {:arrive, {module, id}, public_info, from}) 58 | end 59 | 60 | def place_item(loc_id, item_ref, item_info) do 61 | GenServer.call(via_tuple(loc_id), {:place_item, item_ref, item_info}) 62 | end 63 | 64 | def remove_item(loc_id, item) do 65 | GenServer.cast(via_tuple(loc_id), {:remove_item, item}) 66 | end 67 | 68 | def send_notification(entities, function) do 69 | entities 70 | |> Map.keys 71 | |> Enum.each(function) 72 | end 73 | 74 | def mobs(loc_id, filter) do 75 | GenServer.call(via_tuple(loc_id), {:mobs, filter}) 76 | end 77 | 78 | def mobs(loc_id)do 79 | mobs(loc_id, fn(_x) -> true end) 80 | end 81 | 82 | def handle_call(:location_data, _from, state) do 83 | response = %__MODULE__{ 84 | id: state.id, 85 | name: state.name, 86 | description: state.description, 87 | pathways: state.pathways, 88 | items: %{}, 89 | entities: %{} 90 | } 91 | {:reply, response, state} 92 | end 93 | 94 | # TODO a hand will need to do this. 95 | def handle_call({:place_item, {module, id}, public_info}, _from, state) do 96 | Process.link(GenServer.whereis Item.Corpse.via_tuple(id)) 97 | {:reply, :ok, %Location{state | items: Map.put(state.items, {module, id}, public_info)}} 98 | end 99 | 100 | def handle_call(:look, _from, state) do 101 | living_things = 102 | state.entities 103 | |> Map.values 104 | |> Enum.map(&(&1.name)) 105 | # TODO get string description from public info? 106 | # Maybe define it in Item.Corpse.init ? 107 | items = state.items 108 | |> Map.values 109 | |> Enum.map(&(&1.name)) 110 | seen_things = %{ 111 | living_things: living_things, 112 | items: items, 113 | description: state.description, 114 | exits: state.pathways 115 | } 116 | {:reply, seen_things, state} 117 | end 118 | 119 | def handle_call({:depart, {{module, mob_id}, public_info, to_id}}, _from, state) do 120 | if Enum.member?(Map.keys(state.entities), {module, mob_id}) do 121 | exit_name = 122 | Registry.lookup(PathwayRegistry, {to_id, state.id}) ++ [{nil, "seemingly nowhere"}] 123 | |> List.first 124 | |> elem(1) 125 | 126 | Process.unlink(GenServer.whereis Kernel.apply(module, :via_tuple, [mob_id])) 127 | 128 | send_notification( 129 | state.entities, 130 | fn({module, id}) -> 131 | Kernel.apply(module, :handle, [id, {:depart, public_info, exit_name}]) 132 | end) 133 | {:reply, :ok, %Location{state | entities: Map.delete(state.entities, {module, mob_id})}} 134 | else 135 | IO.inspect "{#{module}, #{mob_id}} is not in loc #{state.id} yet wants to go to #{to_id}." 136 | {:reply, :not_in_location, state} 137 | end 138 | end 139 | 140 | def handle_call({:arrive, {module, mob_id}, public_info, from_loc}, _from, state) do 141 | incoming_exit_name = 142 | Enum.find(state.pathways, %{}, fn(pathway) -> 143 | pathway.from_id == from_loc 144 | end) 145 | |> Map.get(:name, "seemingly nowhere") 146 | 147 | Process.link(GenServer.whereis Kernel.apply(module, :via_tuple, [mob_id])) 148 | 149 | send_notification( 150 | state.entities, 151 | fn({module, id}) -> 152 | Kernel.apply(module, :handle, [id, {:arrive, public_info, incoming_exit_name}]) 153 | end) 154 | { 155 | :reply, 156 | {:ok, state.pathways}, 157 | %Location{state | entities: Map.put(state.entities, {module, mob_id}, public_info)} 158 | } 159 | end 160 | 161 | def handle_call({:mobs, filter}, _from, state) do 162 | mobs = Enum.filter(state.entities, filter) 163 | {:reply, mobs, state} 164 | end 165 | 166 | # TODO make it a call 167 | def handle_cast({:remove_item, {module, id}}, state) do 168 | Process.unlink(GenServer.whereis Item.Corpse.via_tuple(id)) 169 | {:noreply, %__MODULE__{state | items: Map.delete(state.items, {module, id})}} 170 | end 171 | 172 | def handle_cast({:announce_death, {{module, mob_id}, public_info}}, state) do 173 | leftover_entities = Map.delete(state.entities, {module, mob_id}) 174 | send_notification( 175 | leftover_entities, 176 | fn({module, id}) -> 177 | Kernel.apply(module, :handle, [id, {:death, public_info}]) 178 | end) 179 | 180 | {:noreply, %Location{state | entities: leftover_entities}} 181 | end 182 | 183 | def handle_cast({:monitor_pathway, pathway_pid}, state) do 184 | Process.link(pathway_pid) 185 | {:noreply, state} 186 | end 187 | 188 | defp launch_known_pathways(id, pathways) do 189 | Enum.each(pathways, fn(pathway) -> 190 | {:ok, pid} = Pathway.start_link pathway 191 | GenServer.cast(via_tuple(id), {:monitor_pathway, pid}) 192 | end) 193 | end 194 | 195 | defp check_for_other_pathways_to_monitor(this_loc_id) do 196 | World.Registry 197 | |> Registry.match("pathway", {this_loc_id, :_}) 198 | |> Enum.map(fn({pid, _}) -> pid end) 199 | |> Enum.each(fn(pid) -> Process.link(pid) end) 200 | end 201 | 202 | def terminate(reason, state) do 203 | Registry.unregister(LocationRegistry, state.id) 204 | reason 205 | end 206 | 207 | end 208 | -------------------------------------------------------------------------------- /lib/dwarlixir/world/pathway.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.World.Pathway do 2 | alias Dwarlixir.World 3 | defstruct [ 4 | :from_id, :to_id, :name 5 | ] 6 | 7 | @type t :: %World.Pathway{ 8 | from_id: String.t, 9 | to_id: String.t, 10 | name: String.t 11 | } 12 | 13 | use GenServer 14 | alias World.PathwayRegistry 15 | 16 | def start_link(state) do 17 | GenServer.start_link(__MODULE__, state, name: via_tuple(state.from_id, state.to_id)) 18 | end 19 | 20 | defp via_tuple(from_id, to_id) do 21 | {:via, Registry, {PathwayRegistry, {from_id, to_id}}} 22 | end 23 | 24 | def init(state) do 25 | Process.flag(:trap_exit, true) 26 | tuple = {state.from_id, state.to_id} 27 | Registry.register(World.Registry, "pathway", tuple) 28 | new_value = fn(_) -> state.name end 29 | PathwayRegistry 30 | |> Registry.update_value(tuple, new_value) 31 | {:ok, state} 32 | end 33 | 34 | def exits(location_id) do 35 | World.Registry 36 | |> Registry.match("pathway", {location_id, :_}) 37 | |> Enum.flat_map(fn({pid, _}) -> Registry.keys(PathwayRegistry, pid) end) 38 | |> Enum.map(fn({_from, id}) -> id end) 39 | end 40 | 41 | def stop(from_id, to_id) do 42 | GenServer.stop(via_tuple(from_id, to_id)) 43 | end 44 | 45 | def terminate(reason, state) do 46 | Registry.unregister(PathwayRegistry, {state.from_id, state.to_id}) 47 | reason 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /lib/dwarlixir/world/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.World.Supervisor do 2 | alias Dwarlixir.World 3 | use Supervisor 4 | 5 | def start_link(opts \\ %{}) do 6 | Supervisor.start_link(__MODULE__, opts, name: __MODULE__) 7 | end 8 | 9 | def init(_args) do 10 | children = [ 11 | World.Location 12 | ] 13 | 14 | # TODO this is a simple_one_for_one, 15 | # was it a mistake to move the registries in here? 16 | # How about the world? 17 | Supervisor.init( 18 | children, 19 | strategy: :simple_one_for_one 20 | ) 21 | # children = [] 22 | # supervise(children, strategy: :one_for_one) 23 | end 24 | 25 | def start_child(opts) do 26 | Supervisor.start_child(__MODULE__, [opts]) 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /lib/dwarlixir/world/world.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.World do 2 | alias Dwarlixir.World 3 | @type t :: [World.Location.t] 4 | use GenServer 5 | 6 | @ets_name :world 7 | @world_map_key :world_map 8 | 9 | def start_link(opts \\ %{}) do 10 | GenServer.start_link(__MODULE__, opts, name: __MODULE__) 11 | end 12 | 13 | def init(%{init: false}) do 14 | :ets.new(@ets_name, [:set, :named_table, :protected]) 15 | common_init([]) 16 | end 17 | 18 | def init(%{init: :simple}) do 19 | :ets.new(@ets_name, [:set, :named_table, :protected]) 20 | children = map_data_old() 21 | common_init(children) 22 | end 23 | def init(%{init: :new}) do 24 | :ets.new(@ets_name, [:set, :named_table, :protected]) 25 | children = map_data() 26 | common_init(children) 27 | end 28 | def init(%{init: world}) do 29 | {:ok, @ets_name} = :ets.file2tab(@ets_name) 30 | [{{:world_map, _world}, children}] = :ets.lookup(@ets_name, {@world_map_key, world}) 31 | common_init(children) 32 | end 33 | 34 | def save_world do 35 | GenServer.call(__MODULE__, :save_world) 36 | end 37 | 38 | defp common_init(children) do 39 | Enum.each(children, &World.Supervisor.start_child/1) 40 | {:ok, %{}} 41 | end 42 | 43 | def handle_call(:save_world, _from, state) do 44 | world_identifier = UUID.uuid4(:hex) 45 | world = 46 | Supervisor.which_children(World.Supervisor) 47 | |> Enum.map(fn({_id, pid, _type, _module}) -> pid end) 48 | |> Enum.map(&Task.async(fn() -> GenServer.call(&1, :location_data) end)) 49 | |> Enum.map(&Task.await/1) 50 | :ets.insert(@ets_name, {{@world_map_key, world_identifier}, world}) 51 | :ets.tab2file(@ets_name, @ets_name) 52 | {:reply, world_identifier, state} 53 | end 54 | 55 | @spec map_data() :: World.t 56 | def map_data, do: World.Generator.call 57 | 58 | @spec map_data_old() :: World.t 59 | def map_data_old do 60 | [ 61 | location("1", "The Broken Drum", "A tired bar that has seen too many fights", 62 | [ 63 | partial_pathway("2", "upstairs"), 64 | partial_pathway("3", "out"), 65 | ]), 66 | location("2", "A quiet room", "This room is above the main room of the Broken Drum, and surprisingly all the noise dies down up here", 67 | [ 68 | partial_pathway("1","down"), 69 | ]), 70 | location("3", "outside", "This is the street outside the Broken Drum", 71 | [ 72 | partial_pathway("1", "drum"), 73 | partial_pathway("4", "east") 74 | ]), 75 | location("4", "a busy street", "The Broken Drum is West of here.", 76 | [ 77 | partial_pathway("3", "west"), 78 | partial_pathway("5", "north") 79 | ]), 80 | location("5", "a dark alley", "It is dark and you are likely to be eaten by a grue.", 81 | [ 82 | partial_pathway("4", "south") 83 | ]) 84 | ] 85 | end 86 | 87 | def location(id, name, desc, pathways) do 88 | %World.Location{ 89 | id: id, 90 | name: name, 91 | description: desc, 92 | pathways: pathways 93 | } 94 | end 95 | 96 | def partial_pathway(from_id, name) do 97 | %{from_id: from_id, name: name} 98 | end 99 | 100 | def random_room_id do 101 | 1 102 | # Registry.match(World.Registry, "location", :_) 103 | # |> Enum.map(fn({_, id}) -> id end) 104 | # |> Enum.random 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Dwarlixir.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :dwarlixir, 7 | version: "0.0.1", 8 | elixir: "~> 1.8", 9 | build_embedded: Mix.env == :prod, 10 | start_permanent: Mix.env == :prod, 11 | deps: deps() 12 | ] 13 | end 14 | 15 | def application do 16 | # Specify extra applications you'll use from Erlang/Elixir 17 | [extra_applications: [:logger, :runtime_tools], 18 | mod: {Dwarlixir.Application, []}] 19 | end 20 | 21 | defp deps do 22 | [ 23 | {:dialyxir, "~> 0.4", only: [:dev], runtime: false}, 24 | {:credo, "~> 0.8.8", only: [:dev], runtime: false}, 25 | {:distillery, "~> 1.0", runtime: false}, 26 | {:libgraph, "~> 0.11.1"}, 27 | {:ranch, "~> 1.7"}, 28 | {:ecstatic, path: "~/src/projects/ecstatic"}, 29 | {:logger_file_backend, "~> 0.0.9"}, 30 | {:faker, "~> 0.9.0"}, 31 | {:uuid, "~> 1.1"}, 32 | {:petick, "~> 0.0.1"}, 33 | {:table_rex, "~> 0.10.0"}, 34 | {:bunt, "~> 0.2.0"} 35 | ] 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 3 | "credo": {:hex, :credo, "0.8.8", "990e7844a8d06ebacd88744a55853a83b74270b8a8461c55a4d0334b8e1736c9", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, 5 | "distillery": {:hex, :distillery, "1.5.2", "eec18b2d37b55b0bcb670cf2bcf64228ed38ce8b046bb30a9b636a6f5a4c0080", [:mix], [], "hexpm"}, 6 | "doc_first_formatter": {:hex, :doc_first_formatter, "0.0.2", "80beee4f52d7038927905e2c2585a21b425e83b02c6949a2d5931f086c991ff4", [:mix], [], "hexpm"}, 7 | "faker": {:hex, :faker, "0.9.0", "b22c55967fbd3413b9e5c121e59e75a553065587cf31e5aa2271b6fae2775cde", [:mix], [], "hexpm"}, 8 | "gen_stage": {:hex, :gen_stage, "0.12.2", "e0e347cbb1ceb5f4e68a526aec4d64b54ad721f0a8b30aa9d28e0ad749419cbb", [:mix], [], "hexpm"}, 9 | "libgraph": {:hex, :libgraph, "0.11.1", "c8e7517db426c8b8dd0feb4f0ae8cc4d21d7d26c0f5d88c3bb5dedffd07a8b34", [:mix], [], "hexpm"}, 10 | "logger_file_backend": {:hex, :logger_file_backend, "0.0.10", "876f9f84ae110781207c54321ffbb62bebe02946fe3c13f0d7c5f5d8ad4fa910", [:mix], [], "hexpm"}, 11 | "petick": {:hex, :petick, "0.0.1", "f29856643ba55f3fdb8b79ba672b85b9256780a9f4a85e545d9221efbe8f423b", [:mix], [], "hexpm"}, 12 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, 13 | "table_rex": {:hex, :table_rex, "0.10.0", "d981954370c00645c32ff7719c105db90ba6a9aa3e951c4d68eaeae3e0bfd40f", [:mix], [], "hexpm"}, 14 | "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm"}, 15 | } 16 | -------------------------------------------------------------------------------- /rel/config.exs: -------------------------------------------------------------------------------- 1 | # Import all plugins from `rel/plugins` 2 | # They can then be used by adding `plugin MyPlugin` to 3 | # either an environment, or release definition, where 4 | # `MyPlugin` is the name of the plugin module. 5 | Path.join(["rel", "plugins", "*.exs"]) 6 | |> Path.wildcard() 7 | |> Enum.map(&Code.eval_file(&1)) 8 | 9 | use Mix.Releases.Config, 10 | # This sets the default release built by `mix release` 11 | default_release: :default, 12 | # This sets the default environment used by `mix release` 13 | default_environment: Mix.env() 14 | 15 | # For a full list of config options for both releases 16 | # and environments, visit https://hexdocs.pm/distillery/configuration.html 17 | 18 | 19 | # You may define one or more environments in this file, 20 | # an environment's settings will override those of a release 21 | # when building in that environment, this combination of release 22 | # and environment configuration is called a profile 23 | 24 | environment :dev do 25 | set dev_mode: true 26 | set include_erts: false 27 | set cookie: :">FZUQ]xH_HGpGaWrUCL51*z_|w}PT5t_