├── .formatter.exs ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── ants.gif ├── assets ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── bsconfig.json ├── index.js ├── lib │ └── js │ │ └── src │ │ ├── components │ │ ├── AntIcon.bs.js │ │ ├── MainComponent.bs.js │ │ ├── NotFoundComponent.bs.js │ │ ├── SimComponent.bs.js │ │ ├── SimStartComponent.bs.js │ │ ├── TileComponent.bs.js │ │ ├── TodoAppComponent.bs.js │ │ ├── TodoInputComponent.bs.js │ │ ├── TodoItemComponent.bs.js │ │ └── WorldComponent.bs.js │ │ ├── domain │ │ ├── Knobs.bs.js │ │ ├── KnobsResponse.bs.js │ │ ├── SimId.bs.js │ │ ├── SimResponse.bs.js │ │ ├── Tile.bs.js │ │ ├── TileResponse.bs.js │ │ ├── TodoItem.bs.js │ │ └── World.bs.js │ │ └── lib │ │ ├── Http.bs.js │ │ └── Utils.bs.js ├── package.json ├── src │ ├── components │ │ ├── AntIcon.re │ │ ├── MainComponent.re │ │ ├── NotFoundComponent.re │ │ ├── SimComponent.re │ │ ├── SimStartComponent.re │ │ ├── TileComponent.re │ │ └── WorldComponent.re │ ├── domain │ │ ├── Knobs.re │ │ ├── KnobsResponse.re │ │ ├── SimId.re │ │ ├── SimResponse.re │ │ ├── Tile.re │ │ ├── TileResponse.re │ │ └── World.re │ └── lib │ │ ├── Http.re │ │ └── Utils.re ├── static │ ├── favicon.ico │ └── robots.txt ├── styles │ ├── components │ │ ├── button.scss │ │ ├── icon.scss │ │ ├── index.scss │ │ ├── not-found.scss │ │ ├── sim-start.scss │ │ ├── sim.scss │ │ ├── tile.scss │ │ └── world.scss │ ├── main.scss │ ├── mixins │ │ ├── fonts.scss │ │ ├── icon.scss │ │ └── layout.scss │ ├── reset.scss │ ├── typography.scss │ └── variables │ │ └── colors.scss ├── webpack.config.js └── yarn.lock ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── test.exs └── test.mocks.exs ├── git_hooks └── pre-commit ├── lib ├── ants.ex ├── ants │ ├── ants │ │ ├── ant.ex │ │ ├── ant_food.ex │ │ ├── ant_id.ex │ │ ├── ant_move.ex │ │ ├── ant_supervisor.ex │ │ ├── ants.ex │ │ ├── move.ex │ │ ├── tile_selector.ex │ │ └── to_home.ex │ ├── application.ex │ ├── shared │ │ ├── knobs.ex │ │ ├── pair.ex │ │ ├── simulation_registry.ex │ │ └── utils.ex │ ├── simulations │ │ ├── render.ex │ │ ├── simulation_id.ex │ │ ├── simulation_supervisor.ex │ │ ├── simulations.ex │ │ └── simulations_supervisor.ex │ └── worlds │ │ ├── point.ex │ │ ├── surroundings.ex │ │ ├── tile.ex │ │ ├── tile_lookup.ex │ │ ├── tile_supervisor.ex │ │ ├── tile_type.ex │ │ ├── world_map.ex │ │ ├── world_map_data.ex │ │ └── worlds.ex ├── ants_web.ex └── ants_web │ ├── channels │ └── user_socket.ex │ ├── controllers │ ├── api │ │ ├── fallback_controller.ex │ │ ├── knob_controller.ex │ │ ├── sim_controller.ex │ │ └── turn_controller.ex │ └── app_controller.ex │ ├── endpoint.ex │ ├── gettext.ex │ ├── router.ex │ ├── templates │ └── layout │ │ └── app.html.eex │ └── views │ ├── api │ ├── knob_view.ex │ ├── sim_view.ex │ ├── tile_view.ex │ └── turn_view.ex │ ├── error_helpers.ex │ ├── error_view.ex │ └── layout_view.ex ├── mix.exs ├── mix.lock ├── priv └── gettext │ ├── en │ └── LC_MESSAGES │ │ └── errors.po │ └── errors.pot └── test ├── ants ├── ants │ ├── ant_food_test.exs │ ├── ant_move_test.exs │ ├── ant_test.exs │ ├── move_test.exs │ └── tile_selector_test.exs ├── shared │ └── utils_test.exs └── worlds │ ├── surroundings_test.exs │ ├── tile_test.exs │ ├── tile_type_test.exs │ └── world_test.exs ├── ants_web ├── controllers │ └── app_controller_test.exs └── views │ ├── app_view_test.exs │ ├── error_view_test.exs │ └── layout_view_test.exs ├── support ├── channel_case.ex ├── conn_case.ex └── mocks.ex └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", ".formatter.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # App artifacts 4 | /_build 5 | /db 6 | /deps 7 | /*.ez 8 | 9 | # Generated on crash by the VM 10 | erl_crash.dump 11 | 12 | # Generated on crash by NPM 13 | npm-debug.log 14 | 15 | # Static artifacts 16 | /assets/node_modules 17 | 18 | # Since we are building assets from assets/, 19 | # we ignore priv/static. You may want to comment 20 | # this depending on your deployment strategy. 21 | /priv/static/ 22 | 23 | # Files matching config/*.secret.exs pattern contain sensitive 24 | # data and you should not commit them into version control. 25 | # 26 | # Alternatively, you may comment the line below and commit the 27 | # secrets files as long as you replace their contents by environment 28 | # variables. 29 | /config/*.secret.exs 30 | 31 | .elixir_ls -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "elixirLS.dialyzerEnabled": true, 4 | "search.exclude": { 5 | "**/node_modules": true, 6 | "_build": true, 7 | "deps": true, 8 | "erl_crash.dump": true 9 | }, 10 | "reason.diagnostics.tools": [ 11 | "merlin", 12 | "bsb" 13 | ] 14 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ants 2 | 3 | ![Ants](./ants.gif) 4 | 5 | To start your Phoenix server: 6 | 7 | * Install dependencies with `mix deps.get` 8 | * Install Node.js dependencies with `cd assets && yarn install` 9 | * Start Phoenix with `mix phx.server` 10 | * Start Reason with `yarn start` in another terminal 11 | 12 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 13 | -------------------------------------------------------------------------------- /ants.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/will-wow/ants/305e827f62090b5ad0242728eb5330ce815a2839/ants.gif -------------------------------------------------------------------------------- /assets/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .merlin 3 | .bsb.lock 4 | npm-debug.log 5 | /lib/bs/ 6 | /node_modules/ -------------------------------------------------------------------------------- /assets/.prettierignore: -------------------------------------------------------------------------------- 1 | lib -------------------------------------------------------------------------------- /assets/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # ants 2 | 3 | Run this project: 4 | 5 | ``` 6 | yarn install 7 | yarn start 8 | # in another tab 9 | yarn webpack 10 | ``` 11 | 12 | After you see the webpack compilation succeed (the `npm run webpack` step), open up the nested html files in `src/*` (**no server needed!**). Then modify whichever file in `src` and refresh the page to see the changes. 13 | 14 | **For more elaborate ReasonReact examples**, please see https://github.com/reasonml-community/reason-react-example 15 | -------------------------------------------------------------------------------- /assets/bsconfig.json: -------------------------------------------------------------------------------- 1 | /* This is the BuckleScript configuration file. Note that this is a comment; 2 | BuckleScript comes with a JSON parser that supports comments and trailing 3 | comma. If this screws with your editor highlighting, please tell us by filing 4 | an issue! */ 5 | { 6 | "name": "react-template", 7 | "reason": { "react-jsx": 2 }, 8 | "sources": [{ "dir": "src", "subdirs": true }], 9 | "package-specs": [ 10 | { 11 | "module": "commonjs" 12 | } 13 | ], 14 | "suffix": ".bs.js", 15 | "namespace": true, 16 | "bs-dependencies": ["reason-react", "bs-fetch", "@glennsl/bs-json"], 17 | 18 | "refmt": 3 19 | } 20 | -------------------------------------------------------------------------------- /assets/index.js: -------------------------------------------------------------------------------- 1 | import "promise-polyfill/src/polyfill"; 2 | 3 | import "./styles/main.scss"; 4 | 5 | import "./lib/js/src/components/MainComponent.bs.js"; 6 | -------------------------------------------------------------------------------- /assets/lib/js/src/components/AntIcon.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var React = require("react"); 5 | var ReasonReact = require("reason-react/lib/js/src/ReasonReact.js"); 6 | var Utils$ReactTemplate = require("../lib/Utils.bs.js"); 7 | 8 | var component = ReasonReact.statelessComponent("AntIcon"); 9 | 10 | function make() { 11 | var newrecord = component.slice(); 12 | newrecord[/* render */9] = (function () { 13 | return React.createElement("span", { 14 | className: "icon" 15 | }, Utils$ReactTemplate.str("🐜")); 16 | }); 17 | return newrecord; 18 | } 19 | 20 | exports.component = component; 21 | exports.make = make; 22 | /* component Not a pure module */ 23 | -------------------------------------------------------------------------------- /assets/lib/js/src/components/MainComponent.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var ReactDOMRe = require("reason-react/lib/js/src/ReactDOMRe.js"); 5 | var Caml_format = require("bs-platform/lib/js/caml_format.js"); 6 | var ReasonReact = require("reason-react/lib/js/src/ReasonReact.js"); 7 | var SimComponent$ReactTemplate = require("./SimComponent.bs.js"); 8 | var NotFoundComponent$ReactTemplate = require("./NotFoundComponent.bs.js"); 9 | var SimStartComponent$ReactTemplate = require("./SimStartComponent.bs.js"); 10 | 11 | function renderForRoute(element) { 12 | return ReactDOMRe.renderToElementWithId(element, "root"); 13 | } 14 | 15 | ReasonReact.Router[/* watchUrl */1]((function (url) { 16 | var match = url[/* path */0]; 17 | var element; 18 | var exit = 0; 19 | if (match) { 20 | if (match[0] === "sim") { 21 | var match$1 = match[1]; 22 | if (match$1 && !match$1[1]) { 23 | element = ReasonReact.element(/* None */0, /* None */0, SimComponent$ReactTemplate.make(Caml_format.caml_int_of_string(match$1[0]), /* array */[])); 24 | } else { 25 | exit = 1; 26 | } 27 | } else { 28 | exit = 1; 29 | } 30 | } else { 31 | element = ReasonReact.element(/* None */0, /* None */0, SimStartComponent$ReactTemplate.make(/* array */[])); 32 | } 33 | if (exit === 1) { 34 | element = ReasonReact.element(/* None */0, /* None */0, NotFoundComponent$ReactTemplate.make(/* array */[])); 35 | } 36 | return ReactDOMRe.renderToElementWithId(element, "root"); 37 | })); 38 | 39 | ReasonReact.Router[/* push */0](window.location.pathname.pathname); 40 | 41 | exports.renderForRoute = renderForRoute; 42 | /* Not a pure module */ 43 | -------------------------------------------------------------------------------- /assets/lib/js/src/components/NotFoundComponent.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var React = require("react"); 5 | var ReasonReact = require("reason-react/lib/js/src/ReasonReact.js"); 6 | var Utils$ReactTemplate = require("../lib/Utils.bs.js"); 7 | 8 | var component = ReasonReact.statelessComponent("NotFound"); 9 | 10 | function make() { 11 | var newrecord = component.slice(); 12 | newrecord[/* render */9] = (function () { 13 | return React.createElement("div", { 14 | className: "not-found" 15 | }, React.createElement("h1", undefined, Utils$ReactTemplate.str("404")), React.createElement("p", undefined, Utils$ReactTemplate.str("The ants got lost!"))); 16 | }); 17 | return newrecord; 18 | } 19 | 20 | exports.component = component; 21 | exports.make = make; 22 | /* component Not a pure module */ 23 | -------------------------------------------------------------------------------- /assets/lib/js/src/components/SimComponent.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var Block = require("bs-platform/lib/js/block.js"); 5 | var Curry = require("bs-platform/lib/js/curry.js"); 6 | var React = require("react"); 7 | var ReasonReact = require("reason-react/lib/js/src/ReasonReact.js"); 8 | var Http$ReactTemplate = require("../lib/Http.bs.js"); 9 | var Knobs$ReactTemplate = require("../domain/Knobs.bs.js"); 10 | var Utils$ReactTemplate = require("../lib/Utils.bs.js"); 11 | var SimResponse$ReactTemplate = require("../domain/SimResponse.bs.js"); 12 | var KnobsResponse$ReactTemplate = require("../domain/KnobsResponse.bs.js"); 13 | var WorldComponent$ReactTemplate = require("./WorldComponent.bs.js"); 14 | 15 | function updateWorld(send, json) { 16 | var world = SimResponse$ReactTemplate.parseWorld(json); 17 | return Curry._1(send, /* UpdateWorld */Block.__(0, [world])); 18 | } 19 | 20 | function updateKnobs(send, json) { 21 | var knobs = KnobsResponse$ReactTemplate.parse(json); 22 | return Curry._1(send, /* UpdateKnobs */Block.__(1, [knobs])); 23 | } 24 | 25 | function fetchWorld(send, simId) { 26 | Http$ReactTemplate.json(Http$ReactTemplate.get("/api/sim/" + (String(simId) + ""))).then((function (json) { 27 | return Promise.resolve(updateWorld(send, json)); 28 | })); 29 | return /* () */0; 30 | } 31 | 32 | function fetchKnobs(send, simId) { 33 | Http$ReactTemplate.json(Http$ReactTemplate.get("/api/sim/" + (String(simId) + "/knob"))).then((function (response) { 34 | return Promise.resolve(updateKnobs(send, response)); 35 | })); 36 | return /* () */0; 37 | } 38 | 39 | function doTurn(send, simId) { 40 | Http$ReactTemplate.post("/api/sim/" + (String(simId) + "/turn")).then((function (response) { 41 | if (response[/* status */1] === 201) { 42 | return Promise.resolve(Curry._1(send, /* Finished */5)); 43 | } else { 44 | return Promise.resolve(updateWorld(send, response[/* json */0])); 45 | } 46 | })); 47 | return /* () */0; 48 | } 49 | 50 | function antStatusMessage(state) { 51 | if (state[/* finished */3]) { 52 | return "The ants found all the food!"; 53 | } else if (state[/* fetching */2]) { 54 | return "The ants are marching!"; 55 | } else { 56 | return "The ants are waiting!"; 57 | } 58 | } 59 | 60 | var component = ReasonReact.reducerComponent("Sim"); 61 | 62 | function make(simId, _) { 63 | var newrecord = component.slice(); 64 | newrecord[/* didMount */4] = (function (param) { 65 | var send = param[/* send */4]; 66 | Curry._1(send, /* FetchWorld */0); 67 | Curry._1(send, /* FetchKnobs */1); 68 | return /* NoUpdate */0; 69 | }); 70 | newrecord[/* render */9] = (function (param) { 71 | var send = param[/* send */4]; 72 | var state = param[/* state */2]; 73 | var match = state[/* fetching */2]; 74 | return React.createElement("div", { 75 | className: "sim" 76 | }, React.createElement("h1", undefined, Utils$ReactTemplate.str("Sim " + (String(simId) + ""))), React.createElement("h2", undefined, Utils$ReactTemplate.str(antStatusMessage(state))), ReasonReact.element(/* None */0, /* None */0, WorldComponent$ReactTemplate.make(state[/* world */0], state[/* knobs */1], /* array */[])), React.createElement("div", { 77 | className: "sim__buttons" 78 | }, React.createElement("button", { 79 | className: "button", 80 | onClick: (function () { 81 | return Curry._1(send, /* DoTurn */2); 82 | }) 83 | }, Utils$ReactTemplate.str("Turn")), React.createElement("button", { 84 | className: "button", 85 | onClick: (function () { 86 | return Curry._1(send, /* Pause */4); 87 | }) 88 | }, match !== 0 ? Utils$ReactTemplate.str("Pause") : Utils$ReactTemplate.str("Play")), React.createElement("a", { 89 | className: "button", 90 | href: "/" 91 | }, React.createElement("span", undefined, Utils$ReactTemplate.str("Back"))))); 92 | }); 93 | newrecord[/* initialState */10] = (function () { 94 | return /* record */[ 95 | /* world : [] */0, 96 | /* knobs */Knobs$ReactTemplate.$$default, 97 | /* fetching : false */0, 98 | /* finished : false */0 99 | ]; 100 | }); 101 | newrecord[/* reducer */12] = (function (action, state) { 102 | if (typeof action === "number") { 103 | switch (action) { 104 | case 0 : 105 | return /* SideEffects */Block.__(2, [(function (param) { 106 | return fetchWorld(param[/* send */4], simId); 107 | })]); 108 | case 1 : 109 | return /* SideEffects */Block.__(2, [(function (param) { 110 | return fetchKnobs(param[/* send */4], simId); 111 | })]); 112 | case 2 : 113 | if (state[/* finished */3]) { 114 | return /* NoUpdate */0; 115 | } else { 116 | return /* SideEffects */Block.__(2, [(function (param) { 117 | return doTurn(param[/* send */4], simId); 118 | })]); 119 | } 120 | case 3 : 121 | return /* SideEffects */Block.__(2, [(function (param) { 122 | if (param[/* state */2][/* fetching */2]) { 123 | return Curry._1(param[/* send */4], /* DoTurn */2); 124 | } else { 125 | return 0; 126 | } 127 | })]); 128 | case 4 : 129 | return /* Update */Block.__(0, [/* record */[ 130 | /* world */state[/* world */0], 131 | /* knobs */state[/* knobs */1], 132 | /* fetching */1 - state[/* fetching */2], 133 | /* finished */state[/* finished */3] 134 | ]]); 135 | case 5 : 136 | return /* Update */Block.__(0, [/* record */[ 137 | /* world */state[/* world */0], 138 | /* knobs */state[/* knobs */1], 139 | /* fetching : false */0, 140 | /* finished : true */1 141 | ]]); 142 | 143 | } 144 | } else if (action.tag) { 145 | return /* Update */Block.__(0, [/* record */[ 146 | /* world */state[/* world */0], 147 | /* knobs */action[0], 148 | /* fetching */state[/* fetching */2], 149 | /* finished */state[/* finished */3] 150 | ]]); 151 | } else { 152 | return /* Update */Block.__(0, [/* record */[ 153 | /* world */action[0], 154 | /* knobs */state[/* knobs */1], 155 | /* fetching */state[/* fetching */2], 156 | /* finished */state[/* finished */3] 157 | ]]); 158 | } 159 | }); 160 | newrecord[/* subscriptions */13] = (function (param) { 161 | var send = param[/* send */4]; 162 | return /* :: */[ 163 | /* Sub */[ 164 | (function () { 165 | return setInterval((function () { 166 | return Curry._1(send, /* DoAutoTurn */3); 167 | }), 50); 168 | }), 169 | (function (prim) { 170 | clearInterval(prim); 171 | return /* () */0; 172 | }) 173 | ], 174 | /* [] */0 175 | ]; 176 | }); 177 | return newrecord; 178 | } 179 | 180 | var str = Utils$ReactTemplate.str; 181 | 182 | var turnLength = 50; 183 | 184 | exports.str = str; 185 | exports.turnLength = turnLength; 186 | exports.updateWorld = updateWorld; 187 | exports.updateKnobs = updateKnobs; 188 | exports.fetchWorld = fetchWorld; 189 | exports.fetchKnobs = fetchKnobs; 190 | exports.doTurn = doTurn; 191 | exports.antStatusMessage = antStatusMessage; 192 | exports.component = component; 193 | exports.make = make; 194 | /* component Not a pure module */ 195 | -------------------------------------------------------------------------------- /assets/lib/js/src/components/SimStartComponent.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var React = require("react"); 5 | var ReasonReact = require("reason-react/lib/js/src/ReasonReact.js"); 6 | var Http$ReactTemplate = require("../lib/Http.bs.js"); 7 | var Utils$ReactTemplate = require("../lib/Utils.bs.js"); 8 | var AntIcon$ReactTemplate = require("./AntIcon.bs.js"); 9 | var SimResponse$ReactTemplate = require("../domain/SimResponse.bs.js"); 10 | 11 | function startSim() { 12 | Http$ReactTemplate.json(Http$ReactTemplate.post("/api/sim")).then((function (json) { 13 | return Promise.resolve(SimResponse$ReactTemplate.parseSimId(json)); 14 | })).then((function (simId) { 15 | return Promise.resolve(ReasonReact.Router[/* push */0]("sim/" + (String(simId) + ""))); 16 | })); 17 | return /* () */0; 18 | } 19 | 20 | var component = ReasonReact.statelessComponent("SimStart"); 21 | 22 | function make() { 23 | var newrecord = component.slice(); 24 | newrecord[/* render */9] = (function () { 25 | return React.createElement("div", { 26 | className: "sim-start" 27 | }, React.createElement("h1", undefined, ReasonReact.element(/* None */0, /* None */0, AntIcon$ReactTemplate.make(/* array */[])), Utils$ReactTemplate.str("Ants"), ReasonReact.element(/* None */0, /* None */0, AntIcon$ReactTemplate.make(/* array */[]))), React.createElement("p", undefined, Utils$ReactTemplate.str("Simulate ant colony foraging behavior, using Elixir processes.")), React.createElement("button", { 28 | onClick: (function () { 29 | return startSim(/* () */0); 30 | }) 31 | }, Utils$ReactTemplate.str("Start a simulation"))); 32 | }); 33 | return newrecord; 34 | } 35 | 36 | exports.startSim = startSim; 37 | exports.component = component; 38 | exports.make = make; 39 | /* component Not a pure module */ 40 | -------------------------------------------------------------------------------- /assets/lib/js/src/components/TileComponent.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var Block = require("bs-platform/lib/js/block.js"); 5 | var Curry = require("bs-platform/lib/js/curry.js"); 6 | var React = require("react"); 7 | var Printf = require("bs-platform/lib/js/printf.js"); 8 | var Pervasives = require("bs-platform/lib/js/pervasives.js"); 9 | var ReasonReact = require("reason-react/lib/js/src/ReasonReact.js"); 10 | 11 | function tileClassName(tile) { 12 | switch (tile.tag | 0) { 13 | case 0 : 14 | case 1 : 15 | return "tile tile--land"; 16 | case 2 : 17 | return "tile tile--home"; 18 | case 3 : 19 | return "tile tile--rock"; 20 | 21 | } 22 | } 23 | 24 | function is_pheromone(tile) { 25 | if (!tile.tag && tile[0][/* pheromone */1] > 0) { 26 | return /* true */1; 27 | } else { 28 | return /* false */0; 29 | } 30 | } 31 | 32 | function has_food(tile) { 33 | if (tile.tag === 1 && tile[0][/* food */1] > 0) { 34 | return /* true */1; 35 | } else { 36 | return /* false */0; 37 | } 38 | } 39 | 40 | function pheromoneOpacity(tile, maxPheromone) { 41 | if (tile.tag) { 42 | return ""; 43 | } else { 44 | var pheromone = tile[0][/* pheromone */1]; 45 | if (pheromone > 0) { 46 | return Pervasives.string_of_float(pheromone / maxPheromone); 47 | } else { 48 | return ""; 49 | } 50 | } 51 | } 52 | 53 | function foodOpacity(tile, maxFood) { 54 | if (tile.tag === 1) { 55 | var food = tile[0][/* food */1]; 56 | if (food > 0) { 57 | return Curry._1(Printf.sprintf(/* Format */[ 58 | /* Float */Block.__(8, [ 59 | /* Float_f */0, 60 | /* No_padding */0, 61 | /* No_precision */0, 62 | /* End_of_format */0 63 | ]), 64 | "%f" 65 | ]), food / maxFood); 66 | } else { 67 | return ""; 68 | } 69 | } else { 70 | return ""; 71 | } 72 | } 73 | 74 | function maxFood(knobs) { 75 | return knobs[/* startingFood */0]; 76 | } 77 | 78 | function maxPheromone(knobs) { 79 | return 5 * knobs[/* pheromoneDeposit */3]; 80 | } 81 | 82 | function antsOfTile(tile) { 83 | return tile[0][/* ants */0]; 84 | } 85 | 86 | function antsClassName(tile, className) { 87 | var match = antsOfTile(tile); 88 | return className + ( 89 | match !== 0 ? " tile--ants" : "" 90 | ); 91 | } 92 | 93 | var component = ReasonReact.statelessComponent("World"); 94 | 95 | function make(tile, knobs, _) { 96 | var newrecord = component.slice(); 97 | newrecord[/* render */9] = (function () { 98 | return React.createElement("div", { 99 | className: antsClassName(tile, tileClassName(tile)) 100 | }, is_pheromone(tile) ? React.createElement("div", { 101 | className: "tile--pheromone", 102 | style: { 103 | opacity: pheromoneOpacity(tile, 5 * knobs[/* pheromoneDeposit */3]) 104 | } 105 | }) : null, has_food(tile) ? React.createElement("div", { 106 | className: "tile--food", 107 | style: { 108 | opacity: foodOpacity(tile, knobs[/* startingFood */0]) 109 | } 110 | }) : null); 111 | }); 112 | return newrecord; 113 | } 114 | 115 | exports.tileClassName = tileClassName; 116 | exports.is_pheromone = is_pheromone; 117 | exports.has_food = has_food; 118 | exports.pheromoneOpacity = pheromoneOpacity; 119 | exports.foodOpacity = foodOpacity; 120 | exports.maxFood = maxFood; 121 | exports.maxPheromone = maxPheromone; 122 | exports.antsOfTile = antsOfTile; 123 | exports.antsClassName = antsClassName; 124 | exports.component = component; 125 | exports.make = make; 126 | /* component Not a pure module */ 127 | -------------------------------------------------------------------------------- /assets/lib/js/src/components/TodoAppComponent.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var List = require("bs-platform/lib/js/list.js"); 5 | var $$Array = require("bs-platform/lib/js/array.js"); 6 | var Block = require("bs-platform/lib/js/block.js"); 7 | var Curry = require("bs-platform/lib/js/curry.js"); 8 | var React = require("react"); 9 | var Pervasives = require("bs-platform/lib/js/pervasives.js"); 10 | var ReasonReact = require("reason-react/lib/js/src/ReasonReact.js"); 11 | var Utils$ReactTemplate = require("../lib/Utils.bs.js"); 12 | var TodoItem$ReactTemplate = require("../domain/TodoItem.bs.js"); 13 | var TodoItemComponent$ReactTemplate = require("./TodoItemComponent.bs.js"); 14 | var TodoInputComponent$ReactTemplate = require("./TodoInputComponent.bs.js"); 15 | 16 | var lastId = [0]; 17 | 18 | function newItem(text) { 19 | lastId[0] = lastId[0] + 1 | 0; 20 | return TodoItem$ReactTemplate.newItem(lastId[0], text); 21 | } 22 | 23 | function toggleOneItem(id, items) { 24 | return List.map((function (param) { 25 | return TodoItem$ReactTemplate.toggleItem(id, param); 26 | }), items); 27 | } 28 | 29 | var component = ReasonReact.reducerComponent("TodoApp"); 30 | 31 | function make() { 32 | var newrecord = component.slice(); 33 | newrecord[/* render */9] = (function (param) { 34 | var items = param[/* state */2][/* items */0]; 35 | var reduce = param[/* reduce */1]; 36 | var numItems = List.length(items); 37 | return React.createElement("div", { 38 | className: "todo" 39 | }, React.createElement("div", { 40 | className: "title" 41 | }, Utils$ReactTemplate.str("What to do"), ReasonReact.element(/* None */0, /* None */0, TodoInputComponent$ReactTemplate.make(Curry._1(reduce, (function (text) { 42 | return /* AddItem */Block.__(0, [text]); 43 | })), /* array */[]))), React.createElement("div", { 44 | className: "items" 45 | }, $$Array.of_list(List.map((function (item) { 46 | return ReasonReact.element(/* Some */[Pervasives.string_of_int(item[/* id */0])], /* None */0, TodoItemComponent$ReactTemplate.make(item, Curry._1(reduce, (function () { 47 | return /* ToggleItem */Block.__(1, [item[/* id */0]]); 48 | })), /* array */[])); 49 | }), items))), React.createElement("div", { 50 | className: "footer" 51 | }, Utils$ReactTemplate.str(Pervasives.string_of_int(numItems) + " items"))); 52 | }); 53 | newrecord[/* initialState */10] = (function () { 54 | return /* record */[/* items : [] */0]; 55 | }); 56 | newrecord[/* reducer */12] = (function (action, param) { 57 | var items = param[/* items */0]; 58 | if (action.tag) { 59 | var id = action[0]; 60 | return /* Update */Block.__(0, [/* record */[/* items */List.map((function (param) { 61 | return TodoItem$ReactTemplate.toggleItem(id, param); 62 | }), items)]]); 63 | } else { 64 | return /* Update */Block.__(0, [/* record */[/* items : :: */[ 65 | newItem(action[0]), 66 | items 67 | ]]]); 68 | } 69 | }); 70 | return newrecord; 71 | } 72 | 73 | exports.lastId = lastId; 74 | exports.newItem = newItem; 75 | exports.toggleOneItem = toggleOneItem; 76 | exports.component = component; 77 | exports.make = make; 78 | /* component Not a pure module */ 79 | -------------------------------------------------------------------------------- /assets/lib/js/src/components/TodoInputComponent.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var Block = require("bs-platform/lib/js/block.js"); 5 | var Curry = require("bs-platform/lib/js/curry.js"); 6 | var React = require("react"); 7 | var ReasonReact = require("reason-react/lib/js/src/ReasonReact.js"); 8 | 9 | var component = ReasonReact.reducerComponent("TodoInput"); 10 | 11 | function valueFromEvent($$event) { 12 | return $$event.target.value; 13 | } 14 | 15 | function make(onSubmit, _) { 16 | var newrecord = component.slice(); 17 | newrecord[/* render */9] = (function (param) { 18 | var text = param[/* state */2]; 19 | var reduce = param[/* reduce */1]; 20 | return React.createElement("input", { 21 | placeholder: "Write something to do", 22 | type: "text", 23 | value: text, 24 | onKeyDown: (function ($$event) { 25 | if ($$event.key === "Enter") { 26 | Curry._1(onSubmit, text); 27 | return Curry._2(reduce, (function () { 28 | return ""; 29 | }), /* () */0); 30 | } else { 31 | return 0; 32 | } 33 | }), 34 | onChange: Curry._1(reduce, (function ($$event) { 35 | return $$event.target.value; 36 | })) 37 | }); 38 | }); 39 | newrecord[/* initialState */10] = (function () { 40 | return ""; 41 | }); 42 | newrecord[/* reducer */12] = (function (newText, _) { 43 | return /* Update */Block.__(0, [newText]); 44 | }); 45 | return newrecord; 46 | } 47 | 48 | exports.component = component; 49 | exports.valueFromEvent = valueFromEvent; 50 | exports.make = make; 51 | /* component Not a pure module */ 52 | -------------------------------------------------------------------------------- /assets/lib/js/src/components/TodoItemComponent.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var Curry = require("bs-platform/lib/js/curry.js"); 5 | var React = require("react"); 6 | var Js_boolean = require("bs-platform/lib/js/js_boolean.js"); 7 | var ReasonReact = require("reason-react/lib/js/src/ReasonReact.js"); 8 | var Utils$ReactTemplate = require("../lib/Utils.bs.js"); 9 | 10 | var component = ReasonReact.statelessComponent("TodoItem"); 11 | 12 | function make(item, onToggle, _) { 13 | var newrecord = component.slice(); 14 | newrecord[/* render */9] = (function () { 15 | return React.createElement("label", { 16 | className: "item" 17 | }, React.createElement("input", { 18 | checked: Js_boolean.to_js_boolean(item[/* completed */2]), 19 | type: "checkbox", 20 | onClick: (function () { 21 | return Curry._1(onToggle, /* () */0); 22 | }) 23 | }), Utils$ReactTemplate.str(item[/* title */1])); 24 | }); 25 | return newrecord; 26 | } 27 | 28 | exports.component = component; 29 | exports.make = make; 30 | /* component Not a pure module */ 31 | -------------------------------------------------------------------------------- /assets/lib/js/src/components/WorldComponent.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var React = require("react"); 5 | var Pervasives = require("bs-platform/lib/js/pervasives.js"); 6 | var ReasonReact = require("reason-react/lib/js/src/ReasonReact.js"); 7 | var Utils$ReactTemplate = require("../lib/Utils.bs.js"); 8 | var TileComponent$ReactTemplate = require("./TileComponent.bs.js"); 9 | 10 | var component = ReasonReact.statelessComponent("World"); 11 | 12 | function make(world, knobs, _) { 13 | var newrecord = component.slice(); 14 | newrecord[/* render */9] = (function () { 15 | return React.createElement("div", { 16 | className: "world" 17 | }, Utils$ReactTemplate.each_element(world, (function (i, row) { 18 | return React.createElement("div", { 19 | key: Pervasives.string_of_int(i), 20 | className: "world__row" 21 | }, Utils$ReactTemplate.each_element(row, (function (i, tile) { 22 | return React.createElement("div", { 23 | key: Pervasives.string_of_int(i), 24 | className: "world__tile" 25 | }, ReasonReact.element(/* None */0, /* None */0, TileComponent$ReactTemplate.make(tile, knobs, /* array */[]))); 26 | }))); 27 | }))); 28 | }); 29 | return newrecord; 30 | } 31 | 32 | exports.component = component; 33 | exports.make = make; 34 | /* component Not a pure module */ 35 | -------------------------------------------------------------------------------- /assets/lib/js/src/domain/Knobs.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | "use strict"; 3 | 4 | var $$default = /* record */ [ 5 | /* startingFood */ 0, 6 | /* mapSize */ 0, 7 | /* startingAnts */ 0, 8 | /* pheromoneDeposit */ 0, 9 | /* pheromoneEvaporationCoefficient */ 0, 10 | /* pheromoneInfluence */ 0 11 | ]; 12 | 13 | exports.$$default = $$default; 14 | exports.default = $$default; 15 | exports.__esModule = true; 16 | /* No side effect */ 17 | -------------------------------------------------------------------------------- /assets/lib/js/src/domain/KnobsResponse.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | "use strict"; 3 | 4 | var Json_decode = require("@glennsl/bs-json/lib/js/src/Json_decode.js"); 5 | 6 | function parse(json) { 7 | return /* record */ [ 8 | /* startingFood */ Json_decode.field( 9 | "starting_food", 10 | Json_decode.$$int, 11 | json 12 | ), 13 | /* mapSize */ Json_decode.field("map_size", Json_decode.$$int, json), 14 | /* startingAnts */ Json_decode.field( 15 | "starting_ants", 16 | Json_decode.$$int, 17 | json 18 | ), 19 | /* pheromoneDeposit */ Json_decode.field( 20 | "pheromone_deposit", 21 | Json_decode.$$float, 22 | json 23 | ), 24 | /* pheromoneEvaporationCoefficient */ Json_decode.field( 25 | "pheromone_evaporation_coefficient", 26 | Json_decode.$$float, 27 | json 28 | ), 29 | /* pheromoneInfluence */ Json_decode.field( 30 | "pheromone_influence", 31 | Json_decode.$$float, 32 | json 33 | ) 34 | ]; 35 | } 36 | 37 | exports.parse = parse; 38 | /* No side effect */ 39 | -------------------------------------------------------------------------------- /assets/lib/js/src/domain/SimId.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | /* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */ 3 | -------------------------------------------------------------------------------- /assets/lib/js/src/domain/SimResponse.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var Json_decode = require("@glennsl/bs-json/lib/js/src/Json_decode.js"); 5 | var TileResponse$ReactTemplate = require("./TileResponse.bs.js"); 6 | 7 | function parse(json) { 8 | return /* record */[ 9 | /* simId */Json_decode.field("sim_id", Json_decode.$$int, json), 10 | /* world */Json_decode.field("world", (function (param) { 11 | return Json_decode.list((function (param) { 12 | return Json_decode.list(TileResponse$ReactTemplate.parse, param); 13 | }), param); 14 | }), json) 15 | ]; 16 | } 17 | 18 | function parseSimId(json) { 19 | return parse(json)[/* simId */0]; 20 | } 21 | 22 | function parseWorld(json) { 23 | return parse(json)[/* world */1]; 24 | } 25 | 26 | exports.parse = parse; 27 | exports.parseSimId = parseSimId; 28 | exports.parseWorld = parseWorld; 29 | /* No side effect */ 30 | -------------------------------------------------------------------------------- /assets/lib/js/src/domain/Tile.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | /* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */ 3 | -------------------------------------------------------------------------------- /assets/lib/js/src/domain/TileResponse.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var Block = require("bs-platform/lib/js/block.js"); 5 | var Json_decode = require("@glennsl/bs-json/lib/js/src/Json_decode.js"); 6 | 7 | function food_tile(ants, json) { 8 | return /* record */[ 9 | /* ants */ants, 10 | /* food */Json_decode.field("food", Json_decode.$$int, json) 11 | ]; 12 | } 13 | 14 | function home_tile(ants, json) { 15 | return /* record */[ 16 | /* ants */ants, 17 | /* food */Json_decode.field("food", Json_decode.$$int, json) 18 | ]; 19 | } 20 | 21 | function land_tile(ants, json) { 22 | return /* record */[ 23 | /* ants */ants, 24 | /* pheromone */Json_decode.field("pheromone", Json_decode.$$float, json) 25 | ]; 26 | } 27 | 28 | function rock_tile(ants, _) { 29 | return /* record */[/* ants */ants]; 30 | } 31 | 32 | function tile(kind, ants, json) { 33 | switch (kind) { 34 | case "food" : 35 | return /* Food */Block.__(1, [food_tile(ants, json)]); 36 | case "home" : 37 | return /* Home */Block.__(2, [home_tile(ants, json)]); 38 | case "land" : 39 | return /* Land */Block.__(0, [land_tile(ants, json)]); 40 | case "rock" : 41 | return /* Rock */Block.__(3, [/* record */[/* ants */ants]]); 42 | default: 43 | return /* Rock */Block.__(3, [/* record */[/* ants */ants]]); 44 | } 45 | } 46 | 47 | function parse(json) { 48 | var metadata_000 = /* kind */Json_decode.field("kind", Json_decode.string, json); 49 | var metadata_001 = /* ants */Json_decode.field("ants", Json_decode.bool, json); 50 | var partial_arg = metadata_001; 51 | var partial_arg$1 = metadata_000; 52 | return /* record */[/* tile */Json_decode.field("tile", (function (param) { 53 | return tile(partial_arg$1, partial_arg, param); 54 | }), json)][/* tile */0]; 55 | } 56 | 57 | exports.food_tile = food_tile; 58 | exports.home_tile = home_tile; 59 | exports.land_tile = land_tile; 60 | exports.rock_tile = rock_tile; 61 | exports.tile = tile; 62 | exports.parse = parse; 63 | /* No side effect */ 64 | -------------------------------------------------------------------------------- /assets/lib/js/src/domain/TodoItem.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | 5 | function newItem(id, text) { 6 | return /* record */[ 7 | /* id */id, 8 | /* title */text, 9 | /* completed : false */0 10 | ]; 11 | } 12 | 13 | function toggleItem(id, item) { 14 | if (item[/* id */0] === id) { 15 | return /* record */[ 16 | /* id */item[/* id */0], 17 | /* title */item[/* title */1], 18 | /* completed */1 - item[/* completed */2] 19 | ]; 20 | } else { 21 | return item; 22 | } 23 | } 24 | 25 | exports.newItem = newItem; 26 | exports.toggleItem = toggleItem; 27 | /* No side effect */ 28 | -------------------------------------------------------------------------------- /assets/lib/js/src/domain/World.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | /* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */ 3 | -------------------------------------------------------------------------------- /assets/lib/js/src/lib/Http.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var Fetch = require("bs-fetch/lib/js/src/Fetch.js"); 5 | 6 | var jsonHeaders = { 7 | "Content-Type": "application/json" 8 | }; 9 | 10 | var getOpts = Fetch.RequestInit[/* make */0](/* Some */[/* Get */0], /* Some */[jsonHeaders], /* None */0, /* None */0, /* None */0, /* None */0, /* None */0, /* None */0, /* None */0, /* None */0, /* None */0)(/* () */0); 11 | 12 | var postOpts = Fetch.RequestInit[/* make */0](/* Some */[/* Post */2], /* Some */[jsonHeaders], /* None */0, /* None */0, /* None */0, /* None */0, /* None */0, /* None */0, /* None */0, /* None */0, /* None */0)(/* () */0); 13 | 14 | var putOpts = Fetch.RequestInit[/* make */0](/* Some */[/* Put */3], /* Some */[jsonHeaders], /* None */0, /* None */0, /* None */0, /* None */0, /* None */0, /* None */0, /* None */0, /* None */0, /* None */0)(/* () */0); 15 | 16 | var deleteOpts = Fetch.RequestInit[/* make */0](/* Some */[/* Delete */4], /* Some */[jsonHeaders], /* None */0, /* None */0, /* None */0, /* None */0, /* None */0, /* None */0, /* None */0, /* None */0, /* None */0)(/* () */0); 17 | 18 | function dataAndStatus(response) { 19 | return response.json().then((function (json) { 20 | return Promise.resolve(/* record */[ 21 | /* json */json, 22 | /* status */response.status 23 | ]); 24 | })); 25 | } 26 | 27 | function get(url) { 28 | return fetch(url, getOpts).then(dataAndStatus); 29 | } 30 | 31 | function post(url) { 32 | return fetch(url, postOpts).then(dataAndStatus); 33 | } 34 | 35 | function put(url, _) { 36 | return fetch(url, putOpts).then(dataAndStatus); 37 | } 38 | 39 | function $$delete(url, _) { 40 | return fetch(url, deleteOpts).then(dataAndStatus); 41 | } 42 | 43 | function json(promise) { 44 | return promise.then((function (response) { 45 | return Promise.resolve(response[/* json */0]); 46 | })); 47 | } 48 | 49 | exports.jsonHeaders = jsonHeaders; 50 | exports.getOpts = getOpts; 51 | exports.postOpts = postOpts; 52 | exports.putOpts = putOpts; 53 | exports.deleteOpts = deleteOpts; 54 | exports.dataAndStatus = dataAndStatus; 55 | exports.get = get; 56 | exports.post = post; 57 | exports.put = put; 58 | exports.$$delete = $$delete; 59 | exports.json = json; 60 | /* getOpts Not a pure module */ 61 | -------------------------------------------------------------------------------- /assets/lib/js/src/lib/Utils.bs.js: -------------------------------------------------------------------------------- 1 | // Generated by BUCKLESCRIPT VERSION 2.1.1, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var List = require("bs-platform/lib/js/list.js"); 5 | var $$Array = require("bs-platform/lib/js/array.js"); 6 | 7 | function str(prim) { 8 | return prim; 9 | } 10 | 11 | function map_with_index(list, _) { 12 | return List.mapi((function (index, item) { 13 | return /* tuple */[ 14 | index, 15 | item 16 | ]; 17 | }), list); 18 | } 19 | 20 | function each_element(list, toElement) { 21 | return $$Array.of_list(List.mapi(toElement, list)); 22 | } 23 | 24 | exports.str = str; 25 | exports.map_with_index = map_with_index; 26 | exports.each_element = each_element; 27 | /* No side effect */ 28 | -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ants", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "build": "bsb -make-world", 6 | "start": "bsb -make-world -w", 7 | "clean": "bsb -clean-world", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "webpack": "webpack -w", 10 | "format": "yarn format-reason && yarn format-prettier", 11 | "format-reason": "refmt --in-place src/**/*.re", 12 | "format-prettier": "prettier --write '**/*.{js,json,scss}'" 13 | }, 14 | "keywords": ["BuckleScript"], 15 | "author": "", 16 | "license": "MIT", 17 | "dependencies": { 18 | "@glennsl/bs-json": "^1.1.0", 19 | "bs-fetch": "^0.2.0", 20 | "promise-polyfill": "^7.0.0", 21 | "react": "^16.2.0", 22 | "react-dom": "^16.2.0", 23 | "reason-react": ">=0.3.1", 24 | "whatwg-fetch": "^2.0.3" 25 | }, 26 | "devDependencies": { 27 | "bs-platform": "^2.1.0", 28 | "copy-webpack-plugin": "^4.3.1", 29 | "css-loader": "^0.28.8", 30 | "node-sass": "^4.7.2", 31 | "prettier": "^1.10.2", 32 | "sass-loader": "^6.0.6", 33 | "style-loader": "^0.19.1", 34 | "webpack": "^3.10.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /assets/src/components/AntIcon.re: -------------------------------------------------------------------------------- 1 | let component = ReasonReact.statelessComponent("AntIcon"); 2 | 3 | let make = _children => { 4 | ...component, 5 | render: (_) => (Utils.str({js|🐜|js})) 6 | }; 7 | -------------------------------------------------------------------------------- /assets/src/components/MainComponent.re: -------------------------------------------------------------------------------- 1 | let renderForRoute = element => 2 | ReactDOMRe.renderToElementWithId(element, "root"); 3 | 4 | ReasonReact.Router.watchUrl(url => 5 | ( 6 | switch url.path { 7 | | [] => 8 | | ["sim", simId] => 9 | | _ => 10 | } 11 | ) 12 | |> renderForRoute 13 | ); 14 | 15 | /* Hack to get router working. */ 16 | [@bs.scope ("window", "location", "pathname")] [@bs.val] 17 | external pathname : string = "pathname"; 18 | 19 | ReasonReact.Router.push(pathname); 20 | -------------------------------------------------------------------------------- /assets/src/components/NotFoundComponent.re: -------------------------------------------------------------------------------- 1 | let component = ReasonReact.statelessComponent("NotFound"); 2 | 3 | let make = _children => { 4 | ...component, 5 | render: _self => 6 |
7 |

(Utils.str("404"))

8 |

(Utils.str("The ants got lost!"))

9 |
10 | }; 11 | -------------------------------------------------------------------------------- /assets/src/components/SimComponent.re: -------------------------------------------------------------------------------- 1 | let str = Utils.str; 2 | 3 | type state = { 4 | world: World.t, 5 | knobs: Knobs.t, 6 | fetching: bool, 7 | finished: bool 8 | }; 9 | 10 | type action = 11 | | FetchWorld 12 | | FetchKnobs 13 | | DoTurn 14 | | DoAutoTurn 15 | | Pause 16 | | Finished 17 | | UpdateWorld(World.t) 18 | | UpdateKnobs(Knobs.t); 19 | 20 | let turnLength = 50; 21 | 22 | let updateWorld = (send, json) => { 23 | let world = json |> SimResponse.parseWorld; 24 | send(UpdateWorld(world)); 25 | }; 26 | 27 | let updateKnobs = (send, json) => { 28 | let knobs = json |> KnobsResponse.parse; 29 | send(UpdateKnobs(knobs)); 30 | }; 31 | 32 | let fetchWorld = (send, simId: SimId.t) : unit => { 33 | let _ = 34 | Js.Promise.( 35 | Http.get({j|/api/sim/$simId|j}) 36 | |> Http.json 37 | |> then_(json => json |> updateWorld(send) |> resolve) 38 | ); 39 | (); 40 | }; 41 | 42 | let fetchKnobs = (send, simId: SimId.t) : unit => { 43 | let _ = 44 | Js.Promise.( 45 | {j|/api/sim/$simId/knob|j} 46 | |> Http.get 47 | |> Http.json 48 | |> then_(response => response |> updateKnobs(send) |> resolve) 49 | ); 50 | (); 51 | }; 52 | 53 | let doTurn = (send, simId: SimId.t) : unit => { 54 | let _ = 55 | Js.Promise.( 56 | {j|/api/sim/$simId/turn|j} 57 | |> Http.post 58 | |> then_((response: Http.t) => 59 | if (response.status == 201) { 60 | send(Finished) |> resolve; 61 | } else { 62 | response.json |> updateWorld(send) |> resolve; 63 | } 64 | ) 65 | ); 66 | (); 67 | }; 68 | 69 | let antStatusMessage = (state: state) : string => 70 | if (state.finished) { 71 | "The ants found all the food!"; 72 | } else if (state.fetching) { 73 | "The ants are marching!"; 74 | } else { 75 | "The ants are waiting!"; 76 | }; 77 | 78 | let component = ReasonReact.reducerComponent("Sim"); 79 | 80 | let make = (~simId: SimId.t, _children) => { 81 | ...component, 82 | initialState: () => { 83 | world: [], 84 | knobs: Knobs.default, 85 | fetching: false, 86 | finished: false 87 | }, 88 | reducer: (action, state) => 89 | switch action { 90 | | UpdateWorld(world) => ReasonReact.Update({...state, world}) 91 | | UpdateKnobs(knobs) => ReasonReact.Update({...state, knobs}) 92 | | DoAutoTurn => 93 | ReasonReact.SideEffects( 94 | ( 95 | ({state, send}) => 96 | if (state.fetching) { 97 | send(DoTurn); 98 | } 99 | ) 100 | ) 101 | | DoTurn => 102 | if (state.finished) { 103 | ReasonReact.NoUpdate; 104 | } else { 105 | ReasonReact.SideEffects((({send}) => doTurn(send, simId))); 106 | } 107 | | Pause => ReasonReact.Update({...state, fetching: ! state.fetching}) 108 | | Finished => 109 | ReasonReact.Update({...state, finished: true, fetching: false}) 110 | | FetchWorld => 111 | ReasonReact.SideEffects((({send}) => fetchWorld(send, simId))) 112 | | FetchKnobs => 113 | ReasonReact.SideEffects((({send}) => fetchKnobs(send, simId))) 114 | }, 115 | didMount: ({send}) => { 116 | send(FetchWorld); 117 | send(FetchKnobs); 118 | ReasonReact.NoUpdate; 119 | }, 120 | subscriptions: ({send}) => [ 121 | Sub( 122 | () => Js.Global.setInterval(() => send(DoAutoTurn), turnLength), 123 | Js.Global.clearInterval 124 | ) 125 | ], 126 | render: ({state, send}) => 127 |
128 |

(str({j|Sim $simId|j}))

129 |

(str(antStatusMessage(state)))

130 | 131 |
132 | 135 | 138 | (str("Back")) 139 |
140 |
141 | }; 142 | -------------------------------------------------------------------------------- /assets/src/components/SimStartComponent.re: -------------------------------------------------------------------------------- 1 | let startSim = () => { 2 | let _ = 3 | Js.Promise.( 4 | "/api/sim" 5 | |> Http.post 6 | |> Http.json 7 | |> then_(json => json |> SimResponse.parseSimId |> resolve) 8 | |> then_((simId: SimId.t) => 9 | {j|sim/$(simId)|j} |> ReasonReact.Router.push |> resolve 10 | ) 11 | ); 12 | (); 13 | }; 14 | 15 | let component = ReasonReact.statelessComponent("SimStart"); 16 | 17 | let make = _children => { 18 | ...component, 19 | render: (_) => 20 |
21 |

(Utils.str("Ants"))

22 |

23 | ( 24 | Utils.str( 25 | "Simulate ant colony foraging behavior, using Elixir processes." 26 | ) 27 | ) 28 |

29 | 32 |
33 | }; 34 | -------------------------------------------------------------------------------- /assets/src/components/TileComponent.re: -------------------------------------------------------------------------------- 1 | let tileClassName = (tile: Tile.t) : string => 2 | switch tile { 3 | | Land(_tile) => "tile tile--land" 4 | | Food(_tile) => "tile tile--land" 5 | | Home(_tile) => "tile tile--home" 6 | | Rock(_tile) => "tile tile--rock" 7 | }; 8 | 9 | let is_pheromone = (tile: Tile.t) : bool => 10 | switch tile { 11 | | Land({pheromone}) when pheromone > 0. => true 12 | | _ => false 13 | }; 14 | 15 | let has_food = (tile: Tile.t) : bool => 16 | switch tile { 17 | | Food({food}) when food > 0 => true 18 | | _ => false 19 | }; 20 | 21 | let pheromoneOpacity = (tile: Tile.t, maxPheromone: float) : string => 22 | switch tile { 23 | | Land({pheromone}) when pheromone > 0. => 24 | string_of_float(pheromone /. maxPheromone) 25 | | _ => "" 26 | }; 27 | 28 | let foodOpacity = (tile: Tile.t, maxFood: float) : string => 29 | switch tile { 30 | | Food({food}) when food > 0 => 31 | Printf.sprintf("%f", float_of_int(food) /. maxFood) 32 | | _ => "" 33 | }; 34 | 35 | let maxFood = (knobs: Knobs.t) : float => float_of_int(knobs.startingFood); 36 | 37 | let maxPheromone = (knobs: Knobs.t) : float => 5. *. knobs.pheromoneDeposit; 38 | 39 | let antsOfTile = (tile: Tile.t) : bool => 40 | switch tile { 41 | | Land({ants}) => ants 42 | | Food({ants}) => ants 43 | | Home({ants}) => ants 44 | | Rock({ants}) => ants 45 | }; 46 | 47 | let antsClassName = (tile: Tile.t, className: string) : string => 48 | className ++ (antsOfTile(tile) ? " tile--ants" : ""); 49 | 50 | let component = ReasonReact.statelessComponent("World"); 51 | 52 | let make = (~tile: Tile.t, ~knobs: Knobs.t, _children) => { 53 | ...component, 54 | render: _self => 55 |
tileClassName |> antsClassName(tile))> 56 | ( 57 | if (is_pheromone(tile)) { 58 |
pheromoneOpacity(tile), 63 | () 64 | ) 65 | ) 66 | />; 67 | } else { 68 | ReasonReact.nullElement; 69 | } 70 | ) 71 | ( 72 | if (has_food(tile)) { 73 |
foodOpacity(tile), 78 | () 79 | ) 80 | ) 81 | />; 82 | } else { 83 | ReasonReact.nullElement; 84 | } 85 | ) 86 |
87 | }; 88 | -------------------------------------------------------------------------------- /assets/src/components/WorldComponent.re: -------------------------------------------------------------------------------- 1 | let component = ReasonReact.statelessComponent("World"); 2 | 3 | let make = (~world: World.t, ~knobs: Knobs.t, _children) => { 4 | ...component, 5 | render: _self => 6 |
7 | ( 8 | Utils.each_element(world, (i, row) => 9 |
10 | ( 11 | Utils.each_element(row, (i, tile) => 12 |
13 | 14 |
15 | ) 16 | ) 17 |
18 | ) 19 | ) 20 |
21 | }; 22 | -------------------------------------------------------------------------------- /assets/src/domain/Knobs.re: -------------------------------------------------------------------------------- 1 | type t = { 2 | startingFood: int, 3 | mapSize: int, 4 | startingAnts: int, 5 | pheromoneDeposit: float, 6 | pheromoneEvaporationCoefficient: float, 7 | pheromoneInfluence: float 8 | }; 9 | 10 | let default = { 11 | startingFood: 0, 12 | mapSize: 0, 13 | startingAnts: 0, 14 | pheromoneDeposit: 0., 15 | pheromoneEvaporationCoefficient: 0., 16 | pheromoneInfluence: 0. 17 | }; 18 | -------------------------------------------------------------------------------- /assets/src/domain/KnobsResponse.re: -------------------------------------------------------------------------------- 1 | let parse = (json: Js.Json.t) : Knobs.t => 2 | Json.Decode.{ 3 | startingFood: json |> field("starting_food", int), 4 | mapSize: json |> field("map_size", int), 5 | startingAnts: json |> field("starting_ants", int), 6 | pheromoneDeposit: json |> field("pheromone_deposit", float), 7 | pheromoneEvaporationCoefficient: 8 | json |> field("pheromone_evaporation_coefficient", float), 9 | pheromoneInfluence: json |> field("pheromone_influence", float) 10 | }; 11 | -------------------------------------------------------------------------------- /assets/src/domain/SimId.re: -------------------------------------------------------------------------------- 1 | type t = int; 2 | -------------------------------------------------------------------------------- /assets/src/domain/SimResponse.re: -------------------------------------------------------------------------------- 1 | type data = { 2 | simId: SimId.t, 3 | world: World.t 4 | }; 5 | 6 | type t = {data}; 7 | 8 | let parse = (json: Js.Json.t) : data => 9 | Json.Decode.{ 10 | simId: json |> field("sim_id", int), 11 | world: json |> field("world", list(list(TileResponse.parse))) 12 | }; 13 | 14 | let parseSimId = (json: Js.Json.t) : SimId.t => parse(json).simId; 15 | 16 | let parseWorld = (json: Js.Json.t) : World.t => parse(json).world; 17 | -------------------------------------------------------------------------------- /assets/src/domain/Tile.re: -------------------------------------------------------------------------------- 1 | type land_tile = { 2 | ants: bool, 3 | pheromone: float 4 | }; 5 | 6 | type food_tile = { 7 | ants: bool, 8 | food: int 9 | }; 10 | 11 | type home_tile = { 12 | ants: bool, 13 | food: int 14 | }; 15 | 16 | type rock_tile = {ants: bool}; 17 | 18 | type t = 19 | | Land(land_tile) 20 | | Food(food_tile) 21 | | Home(home_tile) 22 | | Rock(rock_tile); 23 | -------------------------------------------------------------------------------- /assets/src/domain/TileResponse.re: -------------------------------------------------------------------------------- 1 | type metadata = { 2 | kind: string, 3 | ants: bool 4 | }; 5 | 6 | type data = {tile: Tile.t}; 7 | 8 | let food_tile = (ants, json: Js.Json.t) : Tile.food_tile => 9 | Json.Decode.{food: json |> field("food", int), ants}; 10 | 11 | let home_tile = (ants, json: Js.Json.t) : Tile.home_tile => 12 | Json.Decode.{food: json |> field("food", int), ants}; 13 | 14 | let land_tile = (ants, json: Js.Json.t) : Tile.land_tile => 15 | Json.Decode.{pheromone: json |> field("pheromone", float), ants}; 16 | 17 | let rock_tile = (ants, _json: Js.Json.t) : Tile.rock_tile => {ants: ants}; 18 | 19 | let tile = (kind: string, ants: bool, json: Js.Json.t) : Tile.t => 20 | switch kind { 21 | | "food" => Food(food_tile(ants, json)) 22 | | "land" => Land(land_tile(ants, json)) 23 | | "home" => Home(home_tile(ants, json)) 24 | | "rock" => Rock(rock_tile(ants, json)) 25 | | _ => Rock(rock_tile(ants, json)) 26 | }; 27 | 28 | let parse = (json: Js.Json.t) : Tile.t => { 29 | let metadata = 30 | Json.Decode.{ 31 | kind: json |> field("kind", string), 32 | ants: json |> field("ants", bool) 33 | }; 34 | Json.Decode.{tile: json |> field("tile", tile(metadata.kind, metadata.ants))}. 35 | tile; 36 | }; 37 | -------------------------------------------------------------------------------- /assets/src/domain/World.re: -------------------------------------------------------------------------------- 1 | type row = list(Tile.t); 2 | 3 | type t = list(row); 4 | -------------------------------------------------------------------------------- /assets/src/lib/Http.re: -------------------------------------------------------------------------------- 1 | type t = { 2 | json: Js.Json.t, 3 | status: int 4 | }; 5 | 6 | let jsonHeaders = Fetch.HeadersInit.make({"Content-Type": "application/json"}); 7 | 8 | let getOpts = Fetch.RequestInit.make(~method_=Get, ~headers=jsonHeaders, ()); 9 | 10 | let postOpts = Fetch.RequestInit.make(~method_=Post, ~headers=jsonHeaders, ()); 11 | 12 | let putOpts = Fetch.RequestInit.make(~method_=Put, ~headers=jsonHeaders, ()); 13 | 14 | let deleteOpts = 15 | Fetch.RequestInit.make(~method_=Delete, ~headers=jsonHeaders, ()); 16 | 17 | let dataAndStatus = (response: Fetch.Response.t) : Js.Promise.t(t) => 18 | Js.Promise.( 19 | response 20 | |> Fetch.Response.json 21 | |> then_(json => 22 | {json, status: Fetch.Response.status(response)} |> resolve 23 | ) 24 | ); 25 | 26 | let get = (url: string) : Js.Promise.t(t) => 27 | Js.Promise.(Fetch.fetchWithInit(url, getOpts) |> then_(dataAndStatus)); 28 | 29 | let post = url => 30 | Js.Promise.(Fetch.fetchWithInit(url, postOpts) |> then_(dataAndStatus)); 31 | 32 | let put = (url, body) => 33 | Js.Promise.(Fetch.fetchWithInit(url, putOpts) |> then_(dataAndStatus)); 34 | 35 | let delete = (url, body) => 36 | Js.Promise.(Fetch.fetchWithInit(url, deleteOpts) |> then_(dataAndStatus)); 37 | 38 | let json = (promise: Js.Promise.t(t)) : Js.Promise.t(Js.Json.t) => 39 | promise |> Js.Promise.(then_(response => response.json |> resolve)); 40 | -------------------------------------------------------------------------------- /assets/src/lib/Utils.re: -------------------------------------------------------------------------------- 1 | type indexed('b) = (int, 'b); 2 | 3 | type index = int; 4 | 5 | let str = ReasonReact.stringToElement; 6 | 7 | let map_with_index = (list: list('a), f: 'a => 'b) : list(indexed('b)) => 8 | list |> List.mapi((index, item: 'a) => (index, item)); 9 | 10 | let each_element = 11 | (list: list('a), toElement: (index, 'a) => ReasonReact.reactElement) 12 | : ReasonReact.reactElement => 13 | list |> List.mapi(toElement) |> Array.of_list |> ReasonReact.arrayToElement; 14 | -------------------------------------------------------------------------------- /assets/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/will-wow/ants/305e827f62090b5ad0242728eb5330ce815a2839/assets/static/favicon.ico -------------------------------------------------------------------------------- /assets/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /assets/styles/components/button.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | 6 | padding: 2px 6px; 7 | height: 30px; 8 | } 9 | -------------------------------------------------------------------------------- /assets/styles/components/icon.scss: -------------------------------------------------------------------------------- 1 | @import "../mixins/icon"; 2 | 3 | .icon { 4 | @include icon; 5 | } 6 | -------------------------------------------------------------------------------- /assets/styles/components/index.scss: -------------------------------------------------------------------------------- 1 | @import "./button"; 2 | @import "./icon"; 3 | @import "./not-found"; 4 | @import "./sim"; 5 | @import "./sim-start"; 6 | @import "./world"; 7 | @import "./tile"; 8 | -------------------------------------------------------------------------------- /assets/styles/components/not-found.scss: -------------------------------------------------------------------------------- 1 | @import "../mixins/layout"; 2 | 3 | .not-found { 4 | @include center-page; 5 | } 6 | -------------------------------------------------------------------------------- /assets/styles/components/sim-start.scss: -------------------------------------------------------------------------------- 1 | @import "../mixins/layout"; 2 | 3 | .sim-start { 4 | @include center-page; 5 | } 6 | -------------------------------------------------------------------------------- /assets/styles/components/sim.scss: -------------------------------------------------------------------------------- 1 | @import "../mixins/layout"; 2 | 3 | .sim { 4 | @include center-page; 5 | 6 | &__buttons { 7 | display: flex; 8 | flex-direction: row; 9 | justify-content: space-between; 10 | align-items: center; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /assets/styles/components/tile.scss: -------------------------------------------------------------------------------- 1 | @import "../mixins/icon"; 2 | @import "../variables/colors"; 3 | 4 | .tile { 5 | width: 100%; 6 | height: 100%; 7 | border: $border; 8 | 9 | @mixin fill { 10 | content: ""; 11 | position: absolute; 12 | top: 0; 13 | left: 0; 14 | bottom: 0; 15 | right: 0; 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | } 20 | 21 | &--land { 22 | background: $land; 23 | position: relative; 24 | } 25 | &--pheromone { 26 | @include fill(); 27 | background: $pheromone; 28 | opacity: 0; 29 | } 30 | 31 | &--food { 32 | @include fill(); 33 | background: $food; 34 | opacity: 0; 35 | } 36 | 37 | &--home { 38 | background: $home; 39 | } 40 | 41 | &--rock { 42 | background: $rock; 43 | } 44 | 45 | &--ants { 46 | position: relative; 47 | &:after { 48 | @include fill; 49 | @include icon; 50 | content: "🐜"; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /assets/styles/components/world.scss: -------------------------------------------------------------------------------- 1 | .world { 2 | flex-grow: 1; 3 | width: calc(888px); 4 | max-width: 100vw; 5 | max-height: 100vw; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: space-between; 10 | 11 | &__row { 12 | flex-grow: 1; 13 | display: flex; 14 | flex-direction: row; 15 | justify-content: space-between; 16 | } 17 | 18 | &__tile { 19 | flex-grow: 1; 20 | display: flex; 21 | flex-direction: row; 22 | justify-content: center; 23 | align-items: center; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import "./reset"; 2 | @import "./typography"; 3 | @import "./components/index"; 4 | -------------------------------------------------------------------------------- /assets/styles/mixins/fonts.scss: -------------------------------------------------------------------------------- 1 | @mixin regular-font { 2 | font-family: "Lato", sans-serif; 3 | font-weight: 400; 4 | } 5 | 6 | @mixin bold-font { 7 | font-family: "Lato", sans-serif; 8 | font-weight: 700; 9 | } 10 | -------------------------------------------------------------------------------- /assets/styles/mixins/icon.scss: -------------------------------------------------------------------------------- 1 | @mixin icon { 2 | font-family: "Apple Color Emoji", "Segoe UI Emoji", "NotoColorEmoji", 3 | "Segoe UI Symbol", "Android Emoji", "EmojiSymbols"; 4 | } 5 | -------------------------------------------------------------------------------- /assets/styles/mixins/layout.scss: -------------------------------------------------------------------------------- 1 | @mixin full-page { 2 | height: 100vh; 3 | width: 100vw; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | @mixin min-full-page { 9 | @include full-page; 10 | height: auto; 11 | min-height: 100vh; 12 | } 13 | 14 | @mixin center-page { 15 | @include full-page; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | @mixin center-top-page { 21 | @include min-full-page; 22 | padding: 1rem; 23 | justify-content: flex-start; 24 | align-items: center; 25 | } 26 | -------------------------------------------------------------------------------- /assets/styles/reset.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | div, 4 | span, 5 | applet, 6 | object, 7 | iframe, 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6, 14 | p, 15 | blockquote, 16 | pre, 17 | a, 18 | abbr, 19 | acronym, 20 | address, 21 | big, 22 | cite, 23 | code, 24 | del, 25 | dfn, 26 | em, 27 | img, 28 | ins, 29 | kbd, 30 | q, 31 | s, 32 | samp, 33 | small, 34 | strike, 35 | strong, 36 | sub, 37 | sup, 38 | tt, 39 | var, 40 | b, 41 | u, 42 | i, 43 | center, 44 | dl, 45 | dt, 46 | dd, 47 | ol, 48 | ul, 49 | li, 50 | fieldset, 51 | form, 52 | label, 53 | legend, 54 | table, 55 | caption, 56 | tbody, 57 | tfoot, 58 | thead, 59 | tr, 60 | th, 61 | td, 62 | article, 63 | aside, 64 | canvas, 65 | details, 66 | embed, 67 | figure, 68 | figcaption, 69 | footer, 70 | header, 71 | hgroup, 72 | menu, 73 | nav, 74 | output, 75 | ruby, 76 | section, 77 | summary, 78 | time, 79 | mark, 80 | audio, 81 | video { 82 | margin: 0; 83 | padding: 0; 84 | border: 0; 85 | font-size: 1rem; 86 | font: inherit; 87 | vertical-align: baseline; 88 | } 89 | /* Border Box */ 90 | html { 91 | box-sizing: border-box; 92 | } 93 | *, 94 | *:before, 95 | *:after { 96 | box-sizing: inherit; 97 | } 98 | /* HTML5 display-role reset for older browsers */ 99 | article, 100 | aside, 101 | details, 102 | figcaption, 103 | figure, 104 | footer, 105 | header, 106 | hgroup, 107 | menu, 108 | nav, 109 | section { 110 | display: block; 111 | } 112 | body { 113 | line-height: 1; 114 | } 115 | ol, 116 | ul { 117 | list-style: none; 118 | } 119 | blockquote, 120 | q { 121 | quotes: none; 122 | } 123 | blockquote:before, 124 | blockquote:after, 125 | q:before, 126 | q:after { 127 | content: ""; 128 | content: none; 129 | } 130 | table { 131 | border-collapse: collapse; 132 | border-spacing: 0; 133 | } 134 | button { 135 | background: transparent; 136 | border: none; 137 | border-radius: none; 138 | } 139 | a { 140 | text-decoration: none; 141 | } 142 | -------------------------------------------------------------------------------- /assets/styles/typography.scss: -------------------------------------------------------------------------------- 1 | @import "./mixins/fonts"; 2 | 3 | body { 4 | @include regular-font; 5 | font-size: 16px; 6 | } 7 | 8 | p { 9 | @include regular-font; 10 | font-size: 1rem; 11 | } 12 | 13 | a, 14 | button { 15 | @include regular-font; 16 | font-size: inherit; 17 | text-decoration: none; 18 | } 19 | 20 | h1 { 21 | @include bold-font; 22 | font-size: 2rem; 23 | } 24 | 25 | h2 { 26 | @include bold-font; 27 | font-size: 1.5rem; 28 | } 29 | 30 | h3 { 31 | @include bold-font; 32 | font-size: 1.17rem; 33 | } 34 | 35 | h4 { 36 | @include bold-font; 37 | font-size: 1rem; 38 | } 39 | 40 | h5 { 41 | @include regular-font; 42 | font-size: 0.83rem; 43 | } 44 | 45 | h6 { 46 | @include regular-font; 47 | font-size: 0.67rem; 48 | } 49 | -------------------------------------------------------------------------------- /assets/styles/variables/colors.scss: -------------------------------------------------------------------------------- 1 | $land: #91bf54; 2 | $food: #a87737; 3 | $pheromone: #fffb57; 4 | $home: #c255cc; 5 | $rock: #787878; 6 | $border: none; 7 | $border: 1px solid #111111; 8 | -------------------------------------------------------------------------------- /assets/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 3 | 4 | module.exports = { 5 | entry: ["whatwg-fetch", "./index.js"], 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.scss$/, 10 | use: [ 11 | { 12 | loader: "style-loader" // creates style nodes from JS strings 13 | }, 14 | { 15 | loader: "css-loader" // translates CSS into CommonJS 16 | }, 17 | { 18 | loader: "sass-loader" // compiles Sass to CSS 19 | } 20 | ] 21 | } 22 | ] 23 | }, 24 | plugins: [new CopyWebpackPlugin([{ from: "static", to: ".." }])], 25 | output: { 26 | path: path.join(__dirname, "../priv/static/js"), 27 | filename: "index.js" 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /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 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # Configures the endpoint 9 | config :ants, AntsWeb.Endpoint, 10 | url: [host: "localhost"], 11 | secret_key_base: "tfbe1TK7+TseYbUo7v3ClEe3YdAOAmjUEYbEcWlN0v+h0aQNrV7DBDS+V4XNEnQI", 12 | render_errors: [view: AntsWeb.ErrorView, accepts: ~w(html json)], 13 | pubsub: [name: Ants.PubSub, adapter: Phoenix.PubSub.PG2] 14 | 15 | # Configures Elixir's Logger 16 | config :logger, :console, 17 | format: "$time $metadata[$level] $message\n", 18 | metadata: [:request_id] 19 | 20 | # Import environment specific config. This must remain at the bottom 21 | # of this file so it overrides the configuration defined above. 22 | import_config "#{Mix.env()}.exs" 23 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with brunch.io to recompile .js and .css sources. 9 | config :ants, AntsWeb.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [ 15 | yarn: ["webpack", cd: Path.expand("../assets", __DIR__)] 16 | ] 17 | 18 | # ## SSL Support 19 | # 20 | # In order to use HTTPS in development, a self-signed 21 | # certificate can be generated by running the following 22 | # command from your terminal: 23 | # 24 | # openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem 25 | # 26 | # The `http:` config above can be replaced with: 27 | # 28 | # https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"], 29 | # 30 | # If desired, both `http:` and `https:` keys can be 31 | # configured to run both http and https servers on 32 | # different ports. 33 | 34 | # Watch static and templates for browser reloading. 35 | config :ants, AntsWeb.Endpoint, 36 | live_reload: [ 37 | patterns: [ 38 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 39 | ~r{priv/gettext/.*(po)$}, 40 | ~r{lib/ants_web/views/.*(ex)$}, 41 | ~r{lib/ants_web/templates/.*(eex)$} 42 | ] 43 | ] 44 | 45 | # Do not include metadata nor timestamps in development logs 46 | config :logger, :console, format: "[$level] $message\n" 47 | 48 | # Set a higher stacktrace during development. Avoid configuring such 49 | # in production as building large stacktraces may be expensive. 50 | config :phoenix, :stacktrace_depth, 20 51 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we often load configuration from external 4 | # sources, such as your system environment. For this reason, 5 | # you won't find the :http configuration below, but set inside 6 | # AntsWeb.Endpoint.init/2 when load_from_system_env is 7 | # true. Any dynamic configuration should be done there. 8 | # 9 | # Don't forget to configure the url host to something meaningful, 10 | # Phoenix uses this information when generating URLs. 11 | # 12 | # Finally, we also include the path to a cache manifest 13 | # containing the digested version of static files. This 14 | # manifest is generated by the mix phx.digest task 15 | # which you typically run after static files are built. 16 | config :ants, AntsWeb.Endpoint, 17 | load_from_system_env: true, 18 | url: [host: "example.com", port: 80], 19 | cache_static_manifest: "priv/static/cache_manifest.json" 20 | 21 | # Do not print debug messages in production 22 | config :logger, level: :info 23 | 24 | # ## SSL Support 25 | # 26 | # To get SSL working, you will need to add the `https` key 27 | # to the previous section and set your `:url` port to 443: 28 | # 29 | # config :ants, AntsWeb.Endpoint, 30 | # ... 31 | # url: [host: "example.com", port: 443], 32 | # https: [:inet6, 33 | # port: 443, 34 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 35 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 36 | # 37 | # Where those two env variables return an absolute path to 38 | # the key and cert in disk or a relative path inside priv, 39 | # for example "priv/ssl/server.key". 40 | # 41 | # We also recommend setting `force_ssl`, ensuring no data is 42 | # ever sent via http, always redirecting to https: 43 | # 44 | # config :ants, AntsWeb.Endpoint, 45 | # force_ssl: [hsts: true] 46 | # 47 | # Check `Plug.SSL` for all available options in `force_ssl`. 48 | 49 | # ## Using releases 50 | # 51 | # If you are doing OTP releases, you need to instruct Phoenix 52 | # to start the server for all endpoints: 53 | # 54 | # config :phoenix, :serve_endpoints, true 55 | # 56 | # Alternatively, you can configure exactly which server to 57 | # start per endpoint: 58 | # 59 | # config :ants, AntsWeb.Endpoint, server: true 60 | # 61 | 62 | # Finally import the config/prod.secret.exs 63 | # which should be versioned separately. 64 | import_config "prod.secret.exs" 65 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :ants, AntsWeb.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | 12 | import_config "test.mocks.exs" 13 | -------------------------------------------------------------------------------- /config/test.mocks.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :ants, :worlds, Ants.WorldsMock 4 | config :ants, :tile_supervisor, Ants.Worlds.TileSupervisorMock 5 | config :ants, :tile_lookup, Ants.Worlds.TileLookupMock 6 | -------------------------------------------------------------------------------- /git_hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ELIXIR_NAMES='\.(ex|exs)$' 4 | REASON_NAMES='\.re$' 5 | PRETTIER_NAMES='\.(js|scss|json)$' 6 | 7 | get_files () { 8 | echo $(git diff --cached --name-only --diff-filter=ACM | grep -E $1) 9 | } 10 | 11 | ELIXIR_FILES=$(get_files $ELIXIR_NAMES) 12 | REASON_FILES=$(get_files $REASON_NAMES) 13 | PRETTIER_FILES=$(get_files $PRETTIER_NAMES) 14 | 15 | if [[ ! -z $ELIXIR_FILES ]] 16 | then 17 | echo "==Running mix format==" 18 | echo $ELIXIR_FILES | xargs mix format || exit 1 19 | echo $ELIXIR_FILES | xargs git add 20 | fi 21 | 22 | if [[ ! -z $REASON_FILES ]] 23 | then 24 | echo "==Running refmt==" 25 | echo $REASON_FILES | xargs refmt --in-place || exit 1 26 | echo $REASON_FILES | xargs git add 27 | fi 28 | 29 | if [[ ! -z $PRETTIER_FILES ]] 30 | then 31 | echo "==Running Prettier==" 32 | echo $PRETTIER_FILES | xargs ./assets/node_modules/.bin/prettier --write || exit 1 33 | echo $PRETTIER_FILES | xargs git add 34 | fi 35 | 36 | echo "===Linting Passed===" 37 | -------------------------------------------------------------------------------- /lib/ants.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants do 2 | @moduledoc """ 3 | Ants keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /lib/ants/ants/ant.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Ants.Ant do 2 | use GenServer 3 | 4 | alias __MODULE__ 5 | alias Ants.Ants.AntMove 6 | alias Ants.Ants.AntFood 7 | alias Ants.Worlds 8 | alias Ants.Simulations.SimId 9 | 10 | @type t :: %Ant{ 11 | x: integer, 12 | y: integer, 13 | food?: boolean 14 | } 15 | 16 | @type state :: {SimId.t(), Ant.t()} 17 | 18 | defstruct x: nil, y: nil, food?: false 19 | 20 | ## Client 21 | 22 | def start_link({sim, x, y, opts}) do 23 | GenServer.start_link(__MODULE__, {sim, x, y}, opts) 24 | end 25 | 26 | def get(pid) do 27 | GenServer.call(pid, :get) 28 | end 29 | 30 | def move(pid) do 31 | GenServer.call(pid, :move) 32 | end 33 | 34 | def deposit_pheromones(pid) do 35 | GenServer.call(pid, :deposit_pheromones) 36 | end 37 | 38 | ## Server 39 | 40 | def init({sim, x, y}) do 41 | {:ok, {sim, %Ant{x: x, y: y}}} 42 | end 43 | 44 | @spec handle_call(:get | :move | :deposit_pheromones, any, state) :: {:reply, Ant.t(), state} 45 | def handle_call(:get, _from, state = {_, ant}) do 46 | {:reply, ant, state} 47 | end 48 | 49 | def handle_call(:move, _from, {sim, ant}) do 50 | x = ant.x 51 | y = ant.y 52 | surroundings = Worlds.surroundings(sim, x, y) 53 | 54 | ant = 55 | ant 56 | |> AntMove.move(surroundings) 57 | |> AntFood.deposit_food(sim) 58 | |> AntFood.take_food(sim) 59 | 60 | {:reply, ant, {sim, ant}} 61 | end 62 | 63 | def handle_call(:deposit_pheromones, _from, {sim, ant}) do 64 | AntFood.deposit_pheromones(ant, sim) 65 | {:reply, ant, {sim, ant}} 66 | end 67 | 68 | def terminate(reason, state) do 69 | IO.inspect({"ant killed!", reason, state}) 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/ants/ants/ant_food.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Ants.AntFood do 2 | alias Ants.Worlds 3 | alias Ants.Simulations.SimId 4 | alias Ants.Ants.Ant 5 | 6 | @spec deposit_food(Ant.t(), SimId.t()) :: Ant.t() 7 | def deposit_food(ant = %Ant{food?: false}, _), do: ant 8 | 9 | def deposit_food(ant, sim) do 10 | %Ant{x: x, y: y} = ant 11 | 12 | case Worlds.deposit_food(sim, x, y) do 13 | {:ok, _} -> 14 | %Ant{ant | food?: false} 15 | 16 | {:error, :not_home} -> 17 | ant 18 | end 19 | end 20 | 21 | @spec take_food(Ant.t(), SimId.t()) :: Ant.t() 22 | def take_food(ant = %Ant{food?: true}, _), do: ant 23 | 24 | def take_food(ant, sim) do 25 | %Ant{x: x, y: y} = ant 26 | 27 | case Worlds.take_food(sim, x, y) do 28 | {:ok, _} -> 29 | %Ant{ant | food?: true} 30 | 31 | {:error, :not_food} -> 32 | ant 33 | end 34 | end 35 | 36 | @spec deposit_pheromones(Ant.t(), SimId.t()) :: Ant.t() 37 | def deposit_pheromones(ant = %Ant{food?: false}, _), do: ant 38 | 39 | def deposit_pheromones(ant, sim) do 40 | %Ant{x: x, y: y} = ant 41 | 42 | Worlds.deposit_pheromones(sim, x, y) 43 | ant 44 | end 45 | end -------------------------------------------------------------------------------- /lib/ants/ants/ant_id.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Ants.AntId do 2 | use Agent 3 | 4 | alias Ants.Simulations.SimId 5 | alias Ants.Shared.SimRegistry 6 | 7 | @type t :: integer 8 | 9 | def start_link(sim) do 10 | Agent.start_link(fn -> [] end, name: via(sim)) 11 | end 12 | 13 | @spec get(SimId.t()) :: [t] 14 | def get(sim) do 15 | sim 16 | |> via() 17 | |> Agent.get(fn ids -> ids end) 18 | end 19 | 20 | def count(sim) do 21 | count = 22 | sim 23 | |> get() 24 | |> List.first() 25 | 26 | count || 0 27 | end 28 | 29 | @spec next(SimId.t()) :: t 30 | def next(sim) do 31 | sim 32 | |> via() 33 | |> Agent.get_and_update(fn ids -> 34 | case ids do 35 | [] -> 36 | {0, [0]} 37 | 38 | [last_id | _] -> 39 | id = last_id + 1 40 | {id, [id | ids]} 41 | end 42 | end) 43 | end 44 | 45 | defp via(sim) do 46 | SimRegistry.ant_id(sim) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/ants/ants/ant_move.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Ants.AntMove do 2 | alias Ants.Ants.Ant 3 | alias Ants.Ants.Move 4 | alias Ants.Ants.ToHome 5 | alias Ants.Ants.TileSelector 6 | alias Ants.Worlds.Surroundings 7 | alias Ants.Worlds.Tile 8 | alias Ants.Worlds.Tile.{Rock, Food} 9 | 10 | @type location :: {Tile.t(), Enum.index()} 11 | 12 | @center_tile_index 4 13 | 14 | @spec move(Ant.t(), Surroundings.t()) :: Ant.t() 15 | def move(ant = %Ant{food?: true}, surroundings) do 16 | move_toward_home(ant, surroundings) 17 | end 18 | 19 | def move(ant = %Ant{food?: false}, surroundings) do 20 | next_move(ant, surroundings) 21 | end 22 | 23 | @spec move_toward_home(Ant.t(), Surroundings.t()) :: Ant.t() 24 | defp move_toward_home(ant = %Ant{food?: true, x: x, y: y}, surroundings) do 25 | move = ToHome.backwards_move({x, y}) 26 | new_local_index = Move.forward_to_index(move) 27 | tile = Enum.at(surroundings, new_local_index) 28 | 29 | case tile do 30 | %Rock{} -> next_move(ant, surroundings) 31 | _ -> move_ant(move, ant) 32 | end 33 | end 34 | 35 | @spec next_move(Ant.t(), Surroundings.t()) :: Ant.t() 36 | defp next_move(ant, surroundings) do 37 | tile_type = if sees_food?(surroundings), do: :food, else: :land 38 | 39 | surroundings 40 | |> Stream.with_index() 41 | |> Enum.filter(&can_visit/1) 42 | |> TileSelector.select(tile_type) 43 | |> (fn 44 | {:ok, index} -> index 45 | {:error, :blocked} -> raise "ant is trapped!" 46 | end).() 47 | |> update_ant_coords(ant) 48 | end 49 | 50 | @spec can_visit(location) :: boolean 51 | defp can_visit({tile, i}) do 52 | case tile do 53 | %Rock{} -> false 54 | _ -> !(i == @center_tile_index) 55 | end 56 | end 57 | 58 | @spec update_ant_coords(Enum.index(), Ant.t()) :: Ant.t() 59 | defp update_ant_coords(local_index, ant = %Ant{}) do 60 | local_index 61 | |> Move.from_index() 62 | |> move_ant(ant) 63 | end 64 | 65 | defp sees_food?(surroundings) do 66 | surroundings 67 | |> Enum.any?(fn tile -> 68 | case tile do 69 | %Food{} -> true 70 | _ -> false 71 | end 72 | end) 73 | end 74 | 75 | @spec move_ant(Move.t(), Ant.t()) :: Ant.t() 76 | defp move_ant({delta_x, delta_y}, ant = %Ant{x: x, y: y}) do 77 | %Ant{ant | x: x + delta_x, y: y + delta_y} 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/ants/ants/ant_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Ants.AntSupervisor do 2 | use DynamicSupervisor 3 | 4 | alias Ants.Shared.SimRegistry 5 | alias Ants.Simulations.SimId 6 | alias Ants.Ants.AntId 7 | alias Ants.Ants.Ant 8 | 9 | def start_link(sim) do 10 | DynamicSupervisor.start_link(__MODULE__, :ok, name: via(sim)) 11 | end 12 | 13 | @spec start_ant(SimId.t(), integer, integer, AntId.t()) :: Supervisor.on_start_child() 14 | def start_ant(sim, x, y, id) do 15 | DynamicSupervisor.start_child( 16 | via(sim), 17 | {Ant, {sim, x, y, [name: ant_via(sim, id)]}} 18 | ) 19 | end 20 | 21 | @spec get_ant(integer, integer) :: SimRegistry.t() 22 | def get_ant(sim, id) do 23 | sim 24 | |> ant_via(id) 25 | end 26 | 27 | def init(:ok) do 28 | DynamicSupervisor.init(strategy: :one_for_one) 29 | end 30 | 31 | defdelegate via(sim), to: SimRegistry, as: :ant_supervisor 32 | defdelegate ant_via(sim, id), to: SimRegistry, as: :ant 33 | end 34 | -------------------------------------------------------------------------------- /lib/ants/ants/ants.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Ants do 2 | alias Ants.Simulations.SimId 3 | alias Ants.Ants.AntSupervisor 4 | alias Ants.Ants.Ant 5 | alias Ants.Ants.AntId 6 | alias Ants.Worlds.Point 7 | 8 | def create_ants(sim, x, y, ants) do 9 | 1..ants 10 | |> Task.async_stream(fn _ -> 11 | create_ant(sim, x, y) 12 | end) 13 | |> Enum.to_list() 14 | end 15 | 16 | def create_new_ant(sim, x, y, max_ants) do 17 | if AntId.count(sim) < max_ants do 18 | create_ant(sim, x, y) 19 | end 20 | end 21 | 22 | def create_ant(sim, x, y) do 23 | id = AntId.next(sim) 24 | AntSupervisor.start_ant(sim, x, y, id) 25 | end 26 | 27 | @spec print(SimId.t()) :: [Point.t()] 28 | def print(sim) do 29 | sim 30 | |> for_each_ant(&Ant.get/1) 31 | |> Enum.map(fn ant -> 32 | {ant.x, ant.y} 33 | end) 34 | end 35 | 36 | @spec count_food(SimId.t()) :: integer 37 | def count_food(sim) do 38 | sim 39 | |> for_each_ant(fn ant_id -> 40 | ant = Ant.get(ant_id) 41 | if ant.food?, do: 1, else: 0 42 | end) 43 | |> Enum.count() 44 | end 45 | 46 | @spec move_all(SimId.t()) :: any 47 | def move_all(sim) do 48 | for_each_ant(sim, &Ant.move/1) 49 | |> Enum.to_list() 50 | end 51 | 52 | @spec deposit_all_pheromones(SimId.t()) :: any 53 | def deposit_all_pheromones(sim) do 54 | for_each_ant(sim, &Ant.deposit_pheromones/1) 55 | end 56 | 57 | defp for_each_ant(sim, f) do 58 | sim 59 | |> AntId.get() 60 | |> Task.async_stream(fn id -> 61 | sim 62 | |> AntSupervisor.get_ant(id) 63 | |> f.() 64 | end) 65 | |> Stream.map(fn {:ok, ant} -> ant end) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/ants/ants/move.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Ants.Move do 2 | alias Ants.Worlds.Point 3 | 4 | @type delta :: -1 | 0 | 1 5 | @type dx :: delta 6 | @type dy :: delta 7 | @type t :: {dx, dy} 8 | 9 | @row_length 3 10 | @highest_index @row_length * 3 - 1 11 | 12 | @spec x(t) :: dx 13 | def x({x, _}), do: x 14 | 15 | @spec y(t) :: dy 16 | def y({_, y}), do: y 17 | 18 | @spec forward_to_index(t) :: Enum.index() 19 | def forward_to_index(move) do 20 | move 21 | |> to_point() 22 | |> Point.to_index(@row_length) 23 | end 24 | 25 | @spec backward_to_index(t) :: Enum.index() 26 | def backward_to_index(move) do 27 | @highest_index - forward_to_index(move) 28 | end 29 | 30 | @spec to_point(t) :: Point.t() 31 | def to_point({dx, dy}) do 32 | {dx + 1, dy + 1} 33 | end 34 | 35 | def from_index(index) do 36 | index 37 | |> Point.from_index(@row_length) 38 | |> from_point() 39 | end 40 | 41 | @spec from_point(Point.t()) :: t 42 | def from_point({x, y}) do 43 | {x - 1, y - 1} 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/ants/ants/tile_selector.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Ants.TileSelector do 2 | alias Ants.Shared.Knobs 3 | alias Ants.Worlds.Tile 4 | alias Ants.Worlds.Tile.{Food, Land, Home, Rock} 5 | alias Ants.Ants.AntMove 6 | alias Ants.Shared.Utils 7 | 8 | @type rating :: {integer, index} 9 | @type tile_type :: :land | :food 10 | @type on_select :: Utils.maybe(index, :blocked) 11 | 12 | @typep location :: AntMove.location() 13 | @typep index :: Enum.index() 14 | @typep locations :: [location] 15 | 16 | @spec select(locations, tile_type) :: on_select 17 | def select(locations, :land), do: select_land(locations) 18 | def select(locations, :food), do: select_food(locations) 19 | 20 | @spec select_land(locations) :: on_select 21 | defp select_land(locations) do 22 | locations 23 | |> Enum.filter(fn location -> 24 | case location do 25 | {%Land{}, _} -> true 26 | {%Home{}, _} -> true 27 | _ -> false 28 | end 29 | end) 30 | |> Enum.map(&rate_location/1) 31 | |> weighted_select 32 | end 33 | 34 | @spec select_food(locations) :: on_select 35 | defp select_food(locations) do 36 | locations 37 | |> Enum.filter(fn location -> 38 | case location do 39 | {%Food{}, _} -> true 40 | _ -> false 41 | end 42 | end) 43 | |> Enum.map(&rate_location/1) 44 | |> weighted_select 45 | end 46 | 47 | @spec rate_location(location) :: rating 48 | defp rate_location({tile, i}) do 49 | {rate_tile(tile), i} 50 | end 51 | 52 | @spec rate_tile(Tile.t()) :: integer 53 | defp rate_tile(%Food{food: food}), do: food + 1 54 | 55 | defp rate_tile(%Land{pheromone: pheromone}), do: :math.pow(pheromone + 1, pheromone_influence()) 56 | 57 | defp rate_tile(%Home{}), do: 1 58 | defp rate_tile(%Rock{}), do: 0 59 | 60 | @spec weighted_select([rating]) :: on_select 61 | defp weighted_select(ratings) do 62 | case ratings 63 | |> Enum.map(&weighted_pair_of_rating/1) 64 | |> Utils.weighted_select() do 65 | {:ok, index} -> {:ok, index} 66 | {:error, _} -> {:error, :blocked} 67 | end 68 | end 69 | 70 | @spec weighted_pair_of_rating(rating) :: Utils.weighted_pair() 71 | defp weighted_pair_of_rating({rating, index}) do 72 | {index, rating} 73 | end 74 | 75 | @spec pheromone_influence() :: number 76 | defp pheromone_influence() do 77 | Knobs.get(:pheromone_influence) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/ants/ants/to_home.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Ants.ToHome do 2 | alias Ants.Ants.Move 3 | alias Ants.Worlds.Point 4 | 5 | @spec backwards_move(Point.t()) :: Move.t() 6 | def backwards_move({x, y}) do 7 | cond do 8 | x < y -> {0, -1} 9 | x > y -> {-1, 0} 10 | x == y -> {-1, -1} 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/ants/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Application do 2 | use Application 3 | 4 | alias Ants.Shared.SimRegistry 5 | alias Ants.Simulations.SimulationsSupervisor 6 | alias Ants.Simulations.SimId 7 | 8 | # See https://hexdocs.pm/elixir/Application.html 9 | # for more information on OTP Applications 10 | def start(_type, _args) do 11 | import Supervisor.Spec 12 | 13 | # Define workers and child supervisors to be supervised 14 | children = [ 15 | # Start the endpoint when the application starts 16 | supervisor(AntsWeb.Endpoint, []), 17 | {Registry, keys: :unique, name: SimRegistry}, 18 | SimId, 19 | {SimulationsSupervisor, [[]]} 20 | # Start your own worker by calling: Ants.Worker.start_link(arg1, arg2, arg3) 21 | # worker(Ants.Worker, [arg1, arg2, arg3]), 22 | ] 23 | 24 | # See https://hexdocs.pm/elixir/Supervisor.html 25 | # for other strategies and supported options 26 | opts = [strategy: :one_for_one, name: Ants.Supervisor] 27 | Supervisor.start_link(children, opts) 28 | end 29 | 30 | # Tell Phoenix to update the endpoint configuration 31 | # whenever the application is updated. 32 | def config_change(changed, _new, removed) do 33 | AntsWeb.Endpoint.config_change(changed, removed) 34 | :ok 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/ants/shared/knobs.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Shared.Knobs do 2 | alias Ants.Worlds.WorldMapData 3 | 4 | @constants %{ 5 | starting_food: 50, 6 | map_size: WorldMapData.get() |> Enum.count(), 7 | starting_ants: 50 8 | } 9 | 10 | @variables %{ 11 | pheromone_deposit: 1, 12 | pheromone_evaporation_coefficient: 0.03, 13 | pheromone_influence: 2.0 14 | } 15 | 16 | @names Enum.map( 17 | Map.keys(@constants) ++ Map.keys(@variables), 18 | &Atom.to_string/1 19 | ) 20 | 21 | @spec get(atom) :: any 22 | def get(name) do 23 | @variables[name] 24 | end 25 | 26 | @spec constant(atom) :: any 27 | def constant(name) do 28 | @constants[name] 29 | end 30 | 31 | @spec parse(String.t()) :: {:ok, atom} | {:error} 32 | def parse(name) do 33 | if Enum.member?(@names, name) do 34 | {:ok, String.to_atom(name)} 35 | else 36 | {:error} 37 | end 38 | end 39 | 40 | @spec all() :: map() 41 | def all() do 42 | Map.merge(@variables, @constants) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/ants/shared/pair.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Shared.Pair do 2 | @type t(first, second) :: {first, second} 3 | 4 | @spec first(t(any, any)) :: any 5 | def first(pair) do 6 | elem(pair, 0) 7 | end 8 | 9 | @spec last(t(any, any)) :: any 10 | def last(pair) do 11 | elem(pair, 1) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/ants/shared/simulation_registry.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Shared.SimRegistry do 2 | @type t :: {:via, Registry, tuple} 3 | 4 | @spec simulation(integer) :: tuple 5 | def simulation(sim) do 6 | via({sim, :sim}) 7 | end 8 | 9 | @spec tile_supervisor(integer) :: Supervisor.name() 10 | def tile_supervisor(sim) do 11 | via({sim, :tile_supervisor}) 12 | end 13 | 14 | @spec ant_supervisor(integer) :: Supervisor.name() 15 | def ant_supervisor(sim) do 16 | via({sim, :ant_supervisor}) 17 | end 18 | 19 | @spec tile(integer, integer, integer) :: Supervisor.name() 20 | def tile(sim, x, y) do 21 | via({sim, :tile, x, y}) 22 | end 23 | 24 | @spec ant(integer, integer) :: Supervisor.name() 25 | def ant(sim, id) do 26 | via({sim, :ant, id}) 27 | end 28 | 29 | @spec ant_id(integer) :: Supervisor.name() 30 | def ant_id(sim) do 31 | via({sim, :ant_id}) 32 | end 33 | 34 | @spec via(tuple) :: Supervisor.name() 35 | defp via(data) do 36 | {:via, Registry, {__MODULE__, data}} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/ants/shared/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Shared.Utils do 2 | alias Ants.Shared.Pair 3 | 4 | @type weighted_pair :: Pair.t(any, number) 5 | @type on_weighted_select :: maybe(any, atom) 6 | 7 | @type maybe(ok, error) :: {:ok, ok} | {:error, error} 8 | 9 | @typep acc :: Enum.acc() 10 | @typep element :: Enum.element() 11 | @typep index :: Enum.index() 12 | 13 | @spec inc(number) :: number 14 | def inc(n), do: n + 1 15 | 16 | @spec dec(number) :: number 17 | def dec(n), do: n - 1 18 | 19 | @spec inc_by(number) :: (number -> number) 20 | def inc_by(i), do: fn n -> n + i end 21 | 22 | @spec dec_by(number) :: (number -> number) 23 | def dec_by(i), do: fn n -> n - i end 24 | 25 | @spec map_indexed(Enum.t(), ({element, index} -> any)) :: list 26 | def map_indexed(enum, f) do 27 | enum 28 | |> Stream.with_index() 29 | |> Enum.map(f) 30 | end 31 | 32 | @spec reduce_indexed(Enum.t(), acc, ({element, index}, acc -> acc)) :: acc 33 | def reduce_indexed(enum, acc, f) do 34 | enum 35 | |> Stream.with_index() 36 | |> Enum.reduce(f, acc) 37 | end 38 | 39 | @spec log(any, any | [any]) :: nil 40 | def log(data, info) when is_list(info) do 41 | IO.inspect([data | info]) 42 | data 43 | end 44 | 45 | def log(data, info) do 46 | IO.inspect([data, info]) 47 | data 48 | end 49 | 50 | @spec weighted_select([weighted_pair]) :: on_weighted_select 51 | def weighted_select([]), do: {:error, :empty} 52 | 53 | def weighted_select(list) do 54 | total = 55 | list 56 | |> Enum.map(&Pair.last/1) 57 | |> Enum.sum() 58 | 59 | case total do 60 | 0 -> 61 | {:error, :no_weights} 62 | 63 | _ -> 64 | random = :rand.uniform() * total 65 | 66 | {:ok, weighted_select(list, random, 0)} 67 | end 68 | end 69 | 70 | defp weighted_select(list, random, sum) do 71 | case list do 72 | [] -> 73 | raise "bad total" 74 | 75 | [hd | tl] -> 76 | sum = sum + Pair.last(hd) 77 | 78 | if sum >= random do 79 | Pair.first(hd) 80 | else 81 | weighted_select(tl, random, sum) 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/ants/simulations/render.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Simulations.Render do 2 | alias __MODULE__ 3 | alias Ants.Shared.Knobs 4 | alias Ants.Shared.Utils 5 | alias Ants.Worlds 6 | alias Ants.Worlds.Tile 7 | alias Ants.Worlds.Point 8 | alias Ants.Simulations.SimId 9 | alias Ants.Ants 10 | 11 | @map_size Knobs.constant(:map_size) 12 | 13 | @type t :: %Render{tile: Tile.t(), ant: boolean} 14 | @type world :: [[t]] 15 | 16 | defstruct [:tile, :ant] 17 | 18 | @spec data(SimId.t()) :: world 19 | def data(sim) do 20 | tiles = Worlds.all_tiles(sim) 21 | ants = Ants.print(sim) 22 | 23 | ant_indexes = 24 | ants 25 | |> Enum.map(&Point.to_index(&1, @map_size)) 26 | 27 | tiles 28 | |> Utils.map_indexed(fn {tile, i} -> 29 | %{ 30 | tile: tile, 31 | ants: ant?(ant_indexes, i) 32 | } 33 | end) 34 | |> Stream.chunk_every(@map_size) 35 | |> Enum.reverse() 36 | end 37 | 38 | defp ant?(ant_indexes, i) do 39 | Enum.member?(ant_indexes, i) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/ants/simulations/simulation_id.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Simulations.SimId do 2 | use Agent 3 | 4 | @type t :: integer 5 | 6 | def start_link(_) do 7 | Agent.start_link(fn -> 0 end, name: __MODULE__) 8 | end 9 | 10 | @spec next :: t 11 | def next do 12 | Agent.get_and_update(__MODULE__, fn id -> 13 | {id, id + 1} 14 | end) 15 | end 16 | 17 | @spec exists?(t) :: boolean 18 | def exists?(sim_id) do 19 | sim_id <= top() - 1 20 | end 21 | 22 | @spec top :: t 23 | defp top do 24 | Agent.get(__MODULE__, fn id -> 25 | id 26 | end) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/ants/simulations/simulation_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Simulations.SimulationSupervisor do 2 | use Supervisor 3 | 4 | alias Ants.Shared.SimRegistry 5 | alias Ants.Worlds.TileSupervisor 6 | alias Ants.Ants.AntSupervisor 7 | alias Ants.Ants.AntId 8 | 9 | def start_link(sim) do 10 | Supervisor.start_link( 11 | __MODULE__, 12 | sim, 13 | name: via(sim) 14 | ) 15 | end 16 | 17 | def init(sim) do 18 | Supervisor.init( 19 | [ 20 | {TileSupervisor, sim}, 21 | {AntId, sim}, 22 | {AntSupervisor, sim} 23 | ], 24 | strategy: :one_for_one 25 | ) 26 | end 27 | 28 | def via(sim) do 29 | SimRegistry.simulation(sim) 30 | end 31 | end -------------------------------------------------------------------------------- /lib/ants/simulations/simulations.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Simulations do 2 | alias Ants.Shared.Knobs 3 | alias Ants.Simulations.SimulationsSupervisor 4 | alias Ants.Simulations.SimId 5 | alias Ants.Simulations.Render 6 | alias Ants.Worlds 7 | alias Ants.Ants 8 | 9 | @starting_ants Knobs.constant(:starting_ants) 10 | 11 | @spec start :: {:ok, SimId.t()} 12 | def start() do 13 | sim = SimId.next() 14 | 15 | {:ok, _} = SimulationsSupervisor.start_simulation(sim) 16 | 17 | Worlds.create_world(sim) 18 | 19 | {:ok, sim} 20 | end 21 | 22 | @spec get(SimId.t()) :: Render.world() 23 | def get(sim) do 24 | Render.data(sim) 25 | end 26 | 27 | @spec turn(SimId.t()) :: {:error, :done} | {:ok, Render.world()} 28 | def turn(sim) do 29 | if done?(sim) do 30 | {:error, :done} 31 | else 32 | Ants.move_all(sim) 33 | 34 | Ants.deposit_all_pheromones(sim) 35 | Worlds.decay_all_pheromones(sim) 36 | 37 | Ants.create_new_ant(sim, 1, 1, @starting_ants) 38 | 39 | {:ok, Render.data(sim)} 40 | end 41 | end 42 | 43 | @spec done?(SimId.t()) :: boolean 44 | def done?(sim) do 45 | food_in_world = Worlds.count_food(sim) 46 | 47 | food_in_world <= 0 48 | end 49 | 50 | @spec knob(String.t()) :: {:ok, any} | {:error} 51 | def knob(name) when is_binary(name) do 52 | case Knobs.parse(name) do 53 | {:ok, atom} -> 54 | value = Knobs.get(atom) || Knobs.constant(atom) 55 | {:ok, value} 56 | 57 | {:error} -> 58 | {:error} 59 | end 60 | end 61 | 62 | defdelegate all_knobs(), to: Knobs, as: :all 63 | end 64 | -------------------------------------------------------------------------------- /lib/ants/simulations/simulations_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Simulations.SimulationsSupervisor do 2 | use DynamicSupervisor 3 | 4 | alias Ants.Simulations.SimulationSupervisor 5 | 6 | def start_link(_opts) do 7 | DynamicSupervisor.start_link(__MODULE__, :ok, name: __MODULE__) 8 | end 9 | 10 | @spec start_simulation(integer) :: Supervisor.on_start_child() 11 | def start_simulation(sim) do 12 | DynamicSupervisor.start_child(__MODULE__, {SimulationSupervisor, sim}) 13 | end 14 | 15 | @spec end_simulation(integer) :: Supervisor.on_start_child() 16 | def end_simulation(sim) do 17 | child_pid = 18 | sim 19 | |> SimulationSupervisor.via() 20 | |> GenServer.whereis() 21 | 22 | DynamicSupervisor.terminate_child(__MODULE__, child_pid) 23 | end 24 | 25 | def init(:ok) do 26 | DynamicSupervisor.init(strategy: :one_for_one) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/ants/worlds/point.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Worlds.Point do 2 | @type x :: integer 3 | @type y :: integer 4 | @type t :: {x, y} 5 | 6 | @spec to_index(t, integer) :: integer 7 | def to_index({x, y}, size) do 8 | y * size + x 9 | end 10 | 11 | @spec from_index(integer, integer) :: t 12 | def from_index(index, size) do 13 | { 14 | x_of_index(index, size), 15 | y_of_index(index, size) 16 | } 17 | end 18 | 19 | @spec x_of_index(Enum.index(), integer) :: x 20 | defp x_of_index(index, size) do 21 | Integer.mod(index, size) 22 | end 23 | 24 | @spec y_of_index(Enum.index(), integer) :: y 25 | defp y_of_index(index, size) do 26 | Integer.floor_div(index, size) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/ants/worlds/surroundings.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Worlds.Surroundings do 2 | alias Ants.Worlds.Tile 3 | alias Ants.Worlds.TileLookup 4 | alias Ants.Worlds.Point 5 | alias Ants.Simulations.SimId 6 | 7 | @type t :: [Tile.t()] 8 | 9 | @lookup Application.get_env(:ants, :tile_lookup, TileLookup) 10 | 11 | @spec surroundings(SimId.t(), integer, integer) :: t 12 | def surroundings(sim, x, y) do 13 | Enum.map(-1..1, fn delta_y -> 14 | Enum.map(-1..1, fn delta_x -> 15 | point_of_offset(x, y, delta_x, delta_y) 16 | end) 17 | end) 18 | |> Enum.concat() 19 | |> Enum.map(fn {x, y} -> 20 | @lookup.lookup(sim, x, y) 21 | end) 22 | end 23 | 24 | @spec point_of_offset(Point.x(), Point.y(), integer, integer) :: Point.t() 25 | defp point_of_offset(x, y, delta_x, delta_y) do 26 | {x + delta_x, y + delta_y} 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/ants/worlds/tile.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Worlds.Tile do 2 | use GenServer, restart: :transient 3 | 4 | alias Ants.Shared.Utils 5 | alias Ants.Shared.Knobs 6 | alias Ants.Worlds.TileType 7 | 8 | ## Structs 9 | 10 | defmodule Land do 11 | @type t :: %Land{pheromone: float} 12 | defstruct pheromone: 0 13 | end 14 | 15 | defmodule Rock do 16 | @type t :: %Rock{} 17 | defstruct [] 18 | end 19 | 20 | defmodule Home do 21 | @type t :: %Home{food: integer} 22 | defstruct food: 0 23 | end 24 | 25 | defmodule Food do 26 | @type t :: %Food{food: integer} 27 | defstruct food: 0 28 | end 29 | 30 | @type t :: Land.t() | Rock.t() | Home.t() | Food.t() 31 | 32 | ## Client 33 | 34 | @spec start_link({TileType.t(), list}) :: GenServer.on_start() 35 | def start_link({type, opts}) do 36 | GenServer.start_link(__MODULE__, type, opts) 37 | end 38 | 39 | def get(pid) do 40 | GenServer.call(pid, :get) 41 | end 42 | 43 | def deposit_pheromones(pid) do 44 | GenServer.call(pid, :deposit_pheromones) 45 | end 46 | 47 | def take_food(pid) do 48 | GenServer.call(pid, :take_food) 49 | end 50 | 51 | def deposit_food(pid) do 52 | GenServer.call(pid, :deposit_food) 53 | end 54 | 55 | def decay_pheromones(pid) do 56 | GenServer.call(pid, :decay_pheromones) 57 | end 58 | 59 | ## Server 60 | 61 | def init(type), do: TileType.tile_of_type(type) 62 | 63 | def handle_call(:get, _from, tile) do 64 | {:reply, tile, tile} 65 | end 66 | 67 | def handle_call(:take_food, _from, tile = %Food{food: food}) when food > 1 do 68 | {:reply, {:ok, 1}, Map.update!(tile, :food, &Utils.dec/1)} 69 | end 70 | 71 | def handle_call(:take_food, _from, %Food{}) do 72 | {:reply, {:ok, 1}, %Land{}} 73 | end 74 | 75 | def handle_call(:take_food, _from, tile) do 76 | {:reply, {:error, :not_food}, tile} 77 | end 78 | 79 | def handle_call(:deposit_pheromones, _from, tile = %Land{}) do 80 | {:reply, {:ok}, Map.update!(tile, :pheromone, Utils.inc_by(pheromone_deposit()))} 81 | end 82 | 83 | def handle_call(:deposit_pheromones, _from, tile) do 84 | {:reply, {:error, :not_land}, tile} 85 | end 86 | 87 | def handle_call(:deposit_food, _from, tile = %Home{}) do 88 | {:reply, {:ok, 1}, Map.update!(tile, :food, &Utils.inc/1)} 89 | end 90 | 91 | def handle_call(:deposit_food, _from, tile) do 92 | {:reply, {:error, :not_home}, tile} 93 | end 94 | 95 | def handle_call(:decay_pheromones, _from, tile = %Land{pheromone: pheromone}) when pheromone > 0 do 96 | tile = %Land{tile | pheromone: (1 - pheromone_evaporation_coefficient()) * pheromone} 97 | 98 | {:reply, tile, tile} 99 | end 100 | 101 | def handle_call(:decay_pheromones, _from, tile) do 102 | {:reply, tile, tile} 103 | end 104 | 105 | @spec pheromone_evaporation_coefficient() :: float 106 | defp pheromone_evaporation_coefficient do 107 | Knobs.get(:pheromone_evaporation_coefficient) 108 | end 109 | 110 | defp pheromone_deposit do 111 | Knobs.get(:pheromone_deposit) 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/ants/worlds/tile_lookup.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Worlds.TileLookup do 2 | alias Ants.Worlds.Tile 3 | alias Ants.Worlds.TileSupervisor 4 | 5 | @callback lookup(integer, integer, integer) :: Tile.t() 6 | @callback get_tile(integer, integer, integer) :: pid 7 | 8 | def lookup(sim, x, y) do 9 | sim 10 | |> get_tile(x, y) 11 | |> Tile.get() 12 | end 13 | 14 | defdelegate get_tile(sim, x, y), to: TileSupervisor 15 | end 16 | -------------------------------------------------------------------------------- /lib/ants/worlds/tile_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Worlds.TileSupervisor do 2 | use DynamicSupervisor 3 | 4 | @callback get_tile(integer, integer, integer) :: pid 5 | 6 | alias Ants.Shared.SimRegistry 7 | alias Ants.Worlds.Tile 8 | 9 | def start_link(sim) do 10 | DynamicSupervisor.start_link(__MODULE__, :ok, name: via(sim)) 11 | end 12 | 13 | def start_tile(sim, tile_type, x, y) do 14 | DynamicSupervisor.start_child(via(sim), { 15 | Tile, 16 | {tile_type, [name: tile_via(sim, x, y)]} 17 | }) 18 | end 19 | 20 | @spec get_tile(integer, integer, integer) :: SimRegistry.t() 21 | def get_tile(sim, x, y) do 22 | sim 23 | |> tile_via(x, y) 24 | end 25 | 26 | def init(:ok) do 27 | DynamicSupervisor.init(strategy: :one_for_one) 28 | end 29 | 30 | defp via(sim) do 31 | SimRegistry.tile_supervisor(sim) 32 | end 33 | 34 | defp tile_via(sim, x, y) do 35 | SimRegistry.tile(sim, x, y) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/ants/worlds/tile_type.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Worlds.TileType do 2 | alias Ants.Shared.Knobs 3 | alias Ants.Worlds.Tile 4 | alias Ants.Worlds.Tile.{Land, Rock, Home, Food} 5 | 6 | @type t :: :land | :light_food | :rock | :home | :food | :light_pheromone | :heavy_pheromone 7 | @type on_tile_of_type :: {:ok, Tile.t()} | {:error, :bad_type} 8 | 9 | @starting_food Knobs.constant(:starting_food) 10 | @light_pheromone 5 11 | @heavy_pheromone 10 12 | 13 | @spec tile_of_type(t) :: on_tile_of_type 14 | def tile_of_type(:land), do: {:ok, %Land{}} 15 | def tile_of_type(:rock), do: {:ok, %Rock{}} 16 | def tile_of_type(:home), do: {:ok, %Home{}} 17 | def tile_of_type(:food), do: {:ok, %Food{food: @starting_food}} 18 | def tile_of_type(:light_food), do: {:ok, %Food{food: 10}} 19 | def tile_of_type(:light_pheromone), do: {:ok, %Land{pheromone: @light_pheromone}} 20 | def tile_of_type(:heavy_pheromone), do: {:ok, %Land{pheromone: @heavy_pheromone}} 21 | def tile_of_type(type), do: {:error, :bad_type, type} 22 | end 23 | -------------------------------------------------------------------------------- /lib/ants/worlds/world_map.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Worlds.WorldMap do 2 | alias Ants.Worlds.TileType 3 | 4 | @type t :: [input_row] 5 | @type tile_types :: [TileType.t()] 6 | @typep input_row :: String.t() 7 | @typep cell :: String.t() 8 | 9 | @spec to_tile_type_list(t) :: tile_types 10 | def to_tile_type_list(world_map) do 11 | world_map 12 | |> to_cell_list() 13 | |> Enum.map(&tile_type_of_cell/1) 14 | end 15 | 16 | @spec to_cell_list(t) :: [cell] 17 | defp to_cell_list(rows) do 18 | rows 19 | |> Enum.map(&split_input_row/1) 20 | |> Enum.reverse() 21 | |> Enum.concat() 22 | end 23 | 24 | @spec split_input_row(input_row) :: [cell] 25 | defp split_input_row(row) do 26 | String.split(row, " ") 27 | end 28 | 29 | @spec tile_type_of_cell(cell) :: TileType.t() 30 | defp tile_type_of_cell("0"), do: :rock 31 | defp tile_type_of_cell("_"), do: :land 32 | defp tile_type_of_cell("F"), do: :food 33 | defp tile_type_of_cell("H"), do: :home 34 | defp tile_type_of_cell("P"), do: :heavy_pheromone 35 | defp tile_type_of_cell("p"), do: :light_pheromone 36 | end 37 | -------------------------------------------------------------------------------- /lib/ants/worlds/world_map_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Worlds.WorldMapData do 2 | @world_map [ 3 | "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0", 4 | "0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 0", 5 | "0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ F _ 0", 6 | "0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 0", 7 | "0 _ _ _ _ _ _ _ F _ _ _ _ _ _ _ _ _ _ 0", 8 | "0 _ _ _ _ F _ _ _ _ _ _ _ _ _ _ _ _ _ 0", 9 | "0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 0", 10 | "0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 0", 11 | "0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 0", 12 | "0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 0", 13 | "0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 0", 14 | "0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 0", 15 | "0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 0", 16 | "0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 0", 17 | "0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 0", 18 | "0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 0", 19 | "0 _ _ _ _ _ _ _ _ _ _ F _ _ _ _ _ _ _ 0", 20 | "0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 0", 21 | "0 H _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 0", 22 | "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0" 23 | ] 24 | 25 | def get() do 26 | @world_map 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/ants/worlds/worlds.ex: -------------------------------------------------------------------------------- 1 | defmodule Ants.Worlds do 2 | alias Ants.Shared.Knobs 3 | alias Ants.Shared.Utils 4 | alias Ants.Simulations.SimId 5 | alias Ants.Worlds.WorldMapData 6 | alias Ants.Worlds.Point 7 | alias Ants.Worlds.Surroundings 8 | alias Ants.Worlds.Tile 9 | alias Ants.Worlds.Tile.Food 10 | alias Ants.Worlds.TileLookup 11 | alias Ants.Worlds.TileSupervisor 12 | alias Ants.Worlds.WorldMap 13 | alias Ants.Worlds.WorldMapData 14 | 15 | @callback create_world(integer, WorldMap.t()) :: :ok 16 | @callback print(integer) :: none 17 | @callback lookup(integer, integer, integer) :: Tile.t() 18 | 19 | @world_map WorldMapData.get() 20 | @map_size Knobs.constant(:map_size) 21 | 22 | @spec create_world(SimId.t()) :: {:ok, home: {integer, integer}} 23 | @spec create_world(SimId.t(), WorldMap.t()) :: {:ok, home: {integer, integer}} 24 | def create_world(sim, world_map \\ @world_map) do 25 | world_map 26 | |> WorldMap.to_tile_type_list() 27 | |> Utils.map_indexed(fn {type, i} -> 28 | {x, y} = Point.from_index(i, @map_size) 29 | 30 | {:ok, _} = TileSupervisor.start_tile(sim, type, x, y) 31 | end) 32 | 33 | # TODO: Find Home location 34 | {:ok, home: {1, 1}} 35 | end 36 | 37 | @spec all_tiles(SimId.t()) :: Enumerable.t(Tile.t()) 38 | def all_tiles(sim) do 39 | all_coords() 40 | |> Enum.map(fn {x, y} -> 41 | sim 42 | |> lookup(x, y) 43 | end) 44 | end 45 | 46 | @spec count_food(SimId.t()) :: integer 47 | def count_food(sim) do 48 | sim 49 | |> all_tiles() 50 | |> Enum.reduce(0, fn tile, acc -> 51 | case tile do 52 | %Food{food: food} -> acc + food 53 | _ -> acc 54 | end 55 | end) 56 | end 57 | 58 | @spec take_food(SimId.t(), integer, integer) :: {:ok, integer} | {:error, :not_food} 59 | def take_food(sim, x, y) do 60 | sim 61 | |> get_tile(x, y) 62 | |> Tile.take_food() 63 | end 64 | 65 | @spec deposit_food(SimId.t(), integer, integer) :: {:ok, integer} | {:error, :not_home} 66 | def deposit_food(sim, x, y) do 67 | sim 68 | |> get_tile(x, y) 69 | |> Tile.deposit_food() 70 | end 71 | 72 | @spec deposit_pheromones(SimId.t(), integer, integer) :: {:ok, integer} | {:error, :not_land} 73 | def deposit_pheromones(sim, x, y) do 74 | sim 75 | |> get_tile(x, y) 76 | |> Tile.deposit_pheromones() 77 | end 78 | 79 | @spec decay_all_pheromones(SimId.t()) :: [Tile.t()] 80 | def decay_all_pheromones(sim) do 81 | all_coords() 82 | |> Enum.map(fn {x, y} -> 83 | sim 84 | |> decay_pheromones(x, y) 85 | end) 86 | end 87 | 88 | defdelegate surroundings(sim, x, y), to: Surroundings 89 | defdelegate lookup(sim, x, y), to: TileLookup 90 | defdelegate get_tile(sim, x, y), to: TileLookup 91 | 92 | defp decay_pheromones(sim, x, y) do 93 | sim 94 | |> get_tile(x, y) 95 | |> Tile.decay_pheromones() 96 | end 97 | 98 | @spec all_coords :: [{integer, integer}] 99 | defp all_coords do 100 | range = 0..(@map_size - 1) 101 | 102 | Enum.map(range, fn y -> 103 | Enum.map(range, fn x -> 104 | {x, y} 105 | end) 106 | end) 107 | |> Enum.concat() 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/ants_web.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use AntsWeb, :controller 9 | use AntsWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: AntsWeb 23 | import Plug.Conn 24 | import AntsWeb.Router.Helpers 25 | import AntsWeb.Gettext 26 | end 27 | end 28 | 29 | def view do 30 | quote do 31 | use Phoenix.View, 32 | root: "lib/ants_web/templates", 33 | namespace: AntsWeb 34 | 35 | # Import convenience functions from controllers 36 | import Phoenix.Controller, only: [get_flash: 2, view_module: 1] 37 | 38 | # Use all HTML functionality (forms, tags, etc) 39 | use Phoenix.HTML 40 | 41 | import AntsWeb.Router.Helpers 42 | import AntsWeb.ErrorHelpers 43 | import AntsWeb.Gettext 44 | end 45 | end 46 | 47 | def router do 48 | quote do 49 | use Phoenix.Router 50 | import Plug.Conn 51 | import Phoenix.Controller 52 | end 53 | end 54 | 55 | def channel do 56 | quote do 57 | use Phoenix.Channel 58 | import AntsWeb.Gettext 59 | end 60 | end 61 | 62 | @doc """ 63 | When used, dispatch to the appropriate controller/view/etc. 64 | """ 65 | defmacro __using__(which) when is_atom(which) do 66 | apply(__MODULE__, which, []) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/ants_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", AntsWeb.RoomChannel 6 | 7 | ## Transports 8 | transport(:websocket, Phoenix.Transports.WebSocket) 9 | # transport :longpoll, Phoenix.Transports.LongPoll 10 | 11 | # Socket params are passed from the client and can 12 | # be used to verify and authenticate a user. After 13 | # verification, you can put default assigns into 14 | # the socket that will be set for all channels, ie 15 | # 16 | # {:ok, assign(socket, :user_id, verified_user_id)} 17 | # 18 | # To deny connection, return `:error`. 19 | # 20 | # See `Phoenix.Token` documentation for examples in 21 | # performing token verification on connect. 22 | def connect(_params, socket) do 23 | {:ok, socket} 24 | end 25 | 26 | # Socket id's are topics that allow you to identify all sockets for a given user: 27 | # 28 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 29 | # 30 | # Would allow you to broadcast a "disconnect" event and terminate 31 | # all active sockets and channels for a given user: 32 | # 33 | # AntsWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 34 | # 35 | # Returning `nil` makes this socket anonymous. 36 | def id(_socket), do: nil 37 | end 38 | -------------------------------------------------------------------------------- /lib/ants_web/controllers/api/fallback_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.Api.FallbackController do 2 | @moduledoc """ 3 | Translates controller action results into valid `Plug.Conn` responses. 4 | 5 | See `Phoenix.Controller.action_fallback/1` for more details. 6 | """ 7 | use AntsWeb, :controller 8 | 9 | alias AntsWeb.ErrorView 10 | 11 | def call(conn, {:error, :not_found}) do 12 | conn 13 | |> put_status(:not_found) 14 | |> render(ErrorView, "404.json", []) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/ants_web/controllers/api/knob_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.Api.KnobController do 2 | use AntsWeb, :controller 3 | 4 | alias AntsWeb.Api.FallbackController 5 | 6 | alias Ants.Simulations 7 | 8 | action_fallback(FallbackController) 9 | 10 | def index(conn, %{"sim_id" => _id}) do 11 | knobs = Simulations.all_knobs() 12 | 13 | conn 14 | |> render("index.json", knobs: knobs) 15 | end 16 | 17 | def show(conn, %{"sim_id" => _id, "id" => name}) do 18 | case Simulations.knob(name) do 19 | {:ok, value} -> 20 | conn 21 | |> render( 22 | "show.json", 23 | knob: %{ 24 | name => value 25 | } 26 | ) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/ants_web/controllers/api/sim_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.Api.SimController do 2 | use AntsWeb, :controller 3 | 4 | alias AntsWeb.Api.FallbackController 5 | alias Ants.Simulations 6 | alias Ants.Simulations.SimId 7 | 8 | action_fallback(FallbackController) 9 | 10 | @spec index(Plug.Conn.t(), any) :: Plug.Conn.t() 11 | def index(conn, _params) do 12 | conn 13 | |> render("index.json", []) 14 | end 15 | 16 | def create(conn, _) do 17 | with {:ok, sim_id} <- Simulations.start(), 18 | world <- Simulations.get(sim_id) do 19 | conn 20 | |> put_status(:created) 21 | |> render("show.json", sim_id: sim_id, world: world) 22 | end 23 | end 24 | 25 | def show(conn, %{"id" => id}) do 26 | with {sim_id, ""} <- Integer.parse(id), 27 | true <- SimId.exists?(sim_id), 28 | world <- Simulations.get(sim_id) do 29 | conn 30 | |> render("show.json", sim_id: sim_id, world: world) 31 | else 32 | _ -> 33 | {:error, :not_found} 34 | end 35 | end 36 | 37 | def turn(conn, %{"id" => id}) do 38 | with {sim_id, ""} <- Integer.parse(id), 39 | true <- SimId.exists?(sim_id), 40 | world <- Simulations.turn(sim_id) do 41 | conn 42 | |> render("show.json", sim_id: sim_id, world: world) 43 | else 44 | _ -> 45 | {:error, :not_found} 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/ants_web/controllers/api/turn_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.Api.TurnController do 2 | use AntsWeb, :controller 3 | 4 | alias AntsWeb.Api.FallbackController 5 | 6 | alias Ants.Simulations 7 | alias Ants.Simulations.SimId 8 | 9 | action_fallback(FallbackController) 10 | 11 | def create(conn, %{"sim_id" => id}) do 12 | with {sim_id, ""} <- Integer.parse(id), 13 | true <- SimId.exists?(sim_id), 14 | {:ok, world} <- Simulations.turn(sim_id) do 15 | conn 16 | |> render("show.json", sim_id: sim_id, world: world) 17 | else 18 | {:error, :done} -> 19 | conn 20 | |> put_status(201) 21 | |> render("done.json", []) 22 | 23 | _ -> 24 | {:error, :not_found} 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/ants_web/controllers/app_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.AppController do 2 | use AntsWeb, :controller 3 | 4 | def index(conn, _params) do 5 | render(conn, "index.html") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/ants_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :ants 3 | 4 | socket("/socket", AntsWeb.UserSocket) 5 | 6 | # Serve at "/" the static files from "priv/static" directory. 7 | # 8 | # You should set gzip to true if you are running phoenix.digest 9 | # when deploying your static files in production. 10 | plug( 11 | Plug.Static, 12 | at: "/", 13 | from: :ants, 14 | gzip: false, 15 | only: ~w(css fonts images js favicon.ico robots.txt) 16 | ) 17 | 18 | # Code reloading can be explicitly enabled under the 19 | # :code_reloader configuration of your endpoint. 20 | if code_reloading? do 21 | socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket) 22 | plug(Phoenix.LiveReloader) 23 | plug(Phoenix.CodeReloader) 24 | end 25 | 26 | plug(Plug.RequestId) 27 | plug(Plug.Logger) 28 | 29 | plug( 30 | Plug.Parsers, 31 | parsers: [:urlencoded, :multipart, :json], 32 | pass: ["*/*"], 33 | json_decoder: Poison 34 | ) 35 | 36 | plug(Plug.MethodOverride) 37 | plug(Plug.Head) 38 | 39 | # The session will be stored in the cookie and signed, 40 | # this means its contents can be read but not tampered with. 41 | # Set :encryption_salt if you would also like to encrypt it. 42 | plug( 43 | Plug.Session, 44 | store: :cookie, 45 | key: "_ants_key", 46 | signing_salt: "JAAg1N6G" 47 | ) 48 | 49 | plug(AntsWeb.Router) 50 | 51 | @doc """ 52 | Callback invoked for dynamically configuring the endpoint. 53 | 54 | It receives the endpoint configuration and checks if 55 | configuration should be loaded from the system environment. 56 | """ 57 | def init(_key, config) do 58 | if config[:load_from_system_env] do 59 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" 60 | {:ok, Keyword.put(config, :http, [:inet6, port: port])} 61 | else 62 | {:ok, config} 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/ants_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import AntsWeb.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :ants 24 | end 25 | -------------------------------------------------------------------------------- /lib/ants_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.Router do 2 | use AntsWeb, :router 3 | 4 | pipeline :browser do 5 | plug(:accepts, ["html"]) 6 | plug(:fetch_session) 7 | plug(:fetch_flash) 8 | plug(:protect_from_forgery) 9 | plug(:put_secure_browser_headers) 10 | end 11 | 12 | pipeline :api do 13 | plug(:accepts, ["json"]) 14 | end 15 | 16 | scope "/api", AntsWeb.Api do 17 | pipe_through(:api) 18 | 19 | resources "/sim", SimController, only: [:index, :create, :show] do 20 | resources("/turn", TurnController, only: [:create]) 21 | resources("/knob", KnobController, only: [:index, :show]) 22 | end 23 | end 24 | 25 | scope "/", AntsWeb do 26 | # Use the default browser stack 27 | pipe_through(:browser) 28 | 29 | get("/*path", AppController, :index) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/ants_web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | "> 11 | 12 | Hello Ants! 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /lib/ants_web/views/api/knob_view.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.Api.KnobView do 2 | use AntsWeb, :view 3 | 4 | def render("index.json", %{knobs: knobs}) do 5 | knobs 6 | end 7 | 8 | def render("show.json", %{knob: data}) do 9 | data 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/ants_web/views/api/sim_view.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.Api.SimView do 2 | use AntsWeb, :view 3 | 4 | alias AntsWeb.Api.TileView 5 | 6 | def render("index.json", _) do 7 | :ok 8 | # render_many(users, SimView, "index.json") 9 | end 10 | 11 | def render("show.json", %{sim_id: sim_id, world: world}) do 12 | %{ 13 | sim_id: sim_id, 14 | world: 15 | Enum.map(world, fn row -> 16 | Enum.map(row, fn cell -> 17 | TileView.render("show.json", cell) 18 | end) 19 | end) 20 | } 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/ants_web/views/api/tile_view.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.Api.TileView do 2 | use AntsWeb, :view 3 | 4 | alias Ants.Worlds.Tile 5 | alias Ants.Worlds.Tile.{Land, Food, Home, Rock} 6 | 7 | @type t :: %{ 8 | kind: String.t(), 9 | ants: boolean, 10 | tile: Tile.t() 11 | } 12 | 13 | @spec render(String.t(), any) :: t 14 | def render("show.json", %{tile: %Land{} = tile, ants: ants}) do 15 | %{ 16 | kind: "land", 17 | ants: ants, 18 | tile: tile 19 | } 20 | end 21 | 22 | def render("show.json", %{tile: %Food{} = tile, ants: ants}) do 23 | %{ 24 | kind: "food", 25 | ants: ants, 26 | tile: tile 27 | } 28 | end 29 | 30 | def render("show.json", %{tile: %Home{} = tile, ants: ants}) do 31 | %{ 32 | kind: "home", 33 | ants: ants, 34 | tile: tile 35 | } 36 | end 37 | 38 | def render("show.json", %{tile: %Rock{} = tile, ants: ants}) do 39 | %{ 40 | kind: "rock", 41 | ants: ants, 42 | tile: tile 43 | } 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/ants_web/views/api/turn_view.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.Api.TurnView do 2 | use AntsWeb, :view 3 | 4 | alias AntsWeb.Api.SimView 5 | 6 | def render("show.json", data) do 7 | SimView.render("show.json", data) 8 | end 9 | 10 | def render("done.json", _) do 11 | %{done: "done"} 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/ants_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | Enum.map(Keyword.get_values(form.errors, field), fn error -> 13 | content_tag(:span, translate_error(error), class: "help-block") 14 | end) 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # Because error messages were defined within Ecto, we must 22 | # call the Gettext module passing our Gettext backend. We 23 | # also use the "errors" domain as translations are placed 24 | # in the errors.po file. 25 | # Ecto will pass the :count keyword if the error message is 26 | # meant to be pluralized. 27 | # On your own code and templates, depending on whether you 28 | # need the message to be pluralized or not, this could be 29 | # written simply as: 30 | # 31 | # dngettext "errors", "1 file", "%{count} files", count 32 | # dgettext "errors", "is invalid" 33 | # 34 | if count = opts[:count] do 35 | Gettext.dngettext(AntsWeb.Gettext, "errors", msg, msg, count, opts) 36 | else 37 | Gettext.dgettext(AntsWeb.Gettext, "errors", msg, opts) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/ants_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.ErrorView do 2 | use AntsWeb, :view 3 | 4 | def render("404.json", _assigns) do 5 | {} 6 | end 7 | 8 | def render("500.html", _assigns) do 9 | "Internal server error" 10 | end 11 | 12 | # In case no render clause matches or no 13 | # template is found, let's render it as 500 14 | def template_not_found(_template, assigns) do 15 | render("500.html", assigns) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ants_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.LayoutView do 2 | use AntsWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Ants.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ants, 7 | version: "0.0.1", 8 | elixir: ">= 1.5.1", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | compilers: [:phoenix, :gettext] ++ Mix.compilers(), 11 | start_permanent: Mix.env() == :prod, 12 | dialyzer: [plt_add_deps: :transitive], 13 | deps: deps() 14 | ] 15 | end 16 | 17 | # Configuration for the OTP application. 18 | # 19 | # Type `mix help compile.app` for more information. 20 | def application do 21 | [ 22 | mod: {Ants.Application, []}, 23 | extra_applications: [:logger, :runtime_tools] 24 | ] 25 | end 26 | 27 | # Specifies which paths to compile per environment. 28 | defp elixirc_paths(:test), do: ["lib", "test/support"] 29 | defp elixirc_paths(_), do: ["lib"] 30 | 31 | # Specifies your project dependencies. 32 | # 33 | # Type `mix help deps` for examples and options. 34 | defp deps do 35 | [ 36 | {:phoenix, "~> 1.3.0"}, 37 | {:phoenix_pubsub, "~> 1.0"}, 38 | {:phoenix_html, "~> 2.10"}, 39 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 40 | {:gettext, "~> 0.11"}, 41 | {:cowboy, "~> 1.0"}, 42 | {:dialyxir, "~> 0.5", only: [:dev], runtime: false}, 43 | {:mox, "~> 0.3", only: :test} 44 | ] 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, 2 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [], [], "hexpm"}, 3 | "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [], [], "hexpm"}, 4 | "file_system": {:hex, :file_system, "0.2.2", "7f1e9de4746f4eb8a4ca8f2fbab582d84a4e40fa394cce7bfcb068b988625b06", [], [], "hexpm"}, 5 | "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [], [], "hexpm"}, 6 | "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [], [], "hexpm"}, 7 | "mox": {:hex, :mox, "0.3.1", "2ab1dce006387a91fbe1e727ba468d3e0691eacfd4e2fc7308d53676ec335c46", [], [], "hexpm"}, 8 | "phoenix": {:hex, :phoenix, "1.3.0", "1c01124caa1b4a7af46f2050ff11b267baa3edb441b45dbf243e979cd4c5891b", [], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "phoenix_html": {:hex, :phoenix_html, "2.10.5", "4f9df6b0fb7422a9440a73182a566cb9cbe0e3ffe8884ef9337ccf284fc1ef0a", [], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 10 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.1.3", "1d178429fc8950b12457d09c6afec247bfe1fcb6f36209e18fbb0221bdfe4d41", [], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"}, 11 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [], [], "hexpm"}, 12 | "plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, 13 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [], [], "hexpm"}, 14 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [], [], "hexpm"}} 15 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here as no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | -------------------------------------------------------------------------------- /test/ants/ants/ant_food_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ants.Ants.AntFoodTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Ants.Ants.Ant 5 | alias Ants.Ants.AntFood 6 | 7 | describe "deposit food" do 8 | test "does nothing without food" do 9 | ant = %Ant{food?: false} 10 | assert AntFood.deposit_food(ant, 1) == ant 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/ants/ants/ant_move_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ants.Ants.AntMoveTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Ants.Ants.Ant 5 | alias Ants.Worlds.TileType 6 | alias Ants.Worlds.Surroundings 7 | alias Ants.Worlds.WorldMap 8 | alias Ants.Ants.AntMove 9 | 10 | describe "an ant at 1, 1" do 11 | setup [:create_home_ant] 12 | 13 | test "takes the only open path", %{ant: ant} do 14 | world_map = [ 15 | "0 _ 0", 16 | "0 _ 0", 17 | "0 0 0" 18 | ] 19 | 20 | surroundings = make_surroundings(world_map) 21 | 22 | assert AntMove.move(ant, surroundings) == %Ant{x: 1, y: 2} 23 | end 24 | 25 | test "goes diagonally", %{ant: ant} do 26 | world_map = [ 27 | "0 0 _", 28 | "0 _ 0", 29 | "0 0 0" 30 | ] 31 | 32 | surroundings = make_surroundings(world_map) 33 | 34 | assert AntMove.move(ant, surroundings) == %Ant{x: 2, y: 2} 35 | end 36 | 37 | test "chooses a land or home", %{ant: ant} do 38 | world_map = [ 39 | "0 _ 0", 40 | "H _ p", 41 | "0 0 0" 42 | ] 43 | 44 | surroundings = make_surroundings(world_map) 45 | 46 | assert Enum.member?( 47 | [ 48 | %Ant{x: 2, y: 1}, 49 | %Ant{x: 1, y: 2}, 50 | %Ant{x: 0, y: 1} 51 | ], 52 | AntMove.move(ant, surroundings) 53 | ) 54 | end 55 | 56 | test "raises when trapped", %{ant: ant} do 57 | world_map = [ 58 | "0 0 0", 59 | "0 _ 0", 60 | "0 0 0" 61 | ] 62 | 63 | surroundings = make_surroundings(world_map) 64 | 65 | assert_raise(RuntimeError, fn -> AntMove.move(ant, surroundings) end) 66 | end 67 | end 68 | 69 | describe "an ant with food" do 70 | setup [:create_home_ant, :with_food] 71 | 72 | test "goes toward home", %{ant: ant} do 73 | world_map = [ 74 | "_ _ _", 75 | "_ _ _", 76 | "H _ _" 77 | ] 78 | 79 | surroundings = make_surroundings(world_map) 80 | 81 | assert AntMove.move(ant, surroundings) == %Ant{ant | x: 0, y: 0} 82 | end 83 | end 84 | 85 | defp create_home_ant(_context) do 86 | %{ant: %Ant{x: 1, y: 1}} 87 | end 88 | 89 | def with_food(context) do 90 | ant = context.ant 91 | %{context | ant: %Ant{ant | food?: true}} 92 | end 93 | 94 | @spec make_surroundings(WorldMap.t()) :: Surroundings.t() 95 | defp make_surroundings(world_map) do 96 | world_map 97 | |> WorldMap.to_tile_type_list() 98 | |> Enum.map(fn type -> 99 | {:ok, tile} = TileType.tile_of_type(type) 100 | tile 101 | end) 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /test/ants/ants/ant_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ants.Ants.AntTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Ants.Ants.Ant 5 | 6 | describe "given an ant" do 7 | setup [:create_ant] 8 | 9 | test "returns an ant at 0, 0", %{ant: ant} do 10 | assert Ant.get(ant) == %Ant{x: 0, y: 0} 11 | end 12 | end 13 | 14 | defp create_ant(_context) do 15 | {:ok, ant} = GenServer.start_link(Ant, {1, 0, 0}) 16 | 17 | %{ant: ant} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/ants/ants/move_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ants.Ants.MoveTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Ants.Ants.Move 5 | 6 | describe "forward_to_index" do 7 | test "goes north east" do 8 | assert Move.forward_to_index({1, 1}) == 8 9 | end 10 | 11 | test "goes south west" do 12 | assert Move.forward_to_index({-1, -1}) == 0 13 | end 14 | 15 | test "goes south" do 16 | assert Move.forward_to_index({0, -1}) == 1 17 | end 18 | end 19 | 20 | describe "backward_to_index" do 21 | test "goes back to south west" do 22 | assert Move.backward_to_index({1, 1}) == 0 23 | end 24 | 25 | test "goes back to north east" do 26 | assert Move.backward_to_index({-1, -1}) == 8 27 | end 28 | 29 | test "goes back to north" do 30 | assert Move.backward_to_index({0, -1}) == 7 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/ants/ants/tile_selector_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ants.Ants.TileSelectorTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Ants.Worlds.Tile.{Food, Land, Rock} 5 | alias Ants.Ants.TileSelector 6 | 7 | test "chooses food" do 8 | locations = [ 9 | {%Land{pheromone: 10}, 2}, 10 | {%Food{food: 10}, 4}, 11 | {%Land{pheromone: 0}, 5}, 12 | {%Rock{}, 6} 13 | ] 14 | 15 | assert TileSelector.select(locations, :food) == {:ok, 4} 16 | end 17 | 18 | test "error when empty" do 19 | locations = [] 20 | 21 | assert TileSelector.select(locations, :land) == {:error, :blocked} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/ants/shared/utils_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ants.Shared.UtilsTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Ants.Shared.Utils 5 | 6 | describe "weighted_select" do 7 | test "selects an item" do 8 | assert Utils.weighted_select([{"nope", 0}, {"yep", 1}]) == {:ok, "yep"} 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/ants/worlds/surroundings_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ants.Worlds.SurroundingsTest do 2 | use ExUnit.Case, async: false 3 | import Mox 4 | 5 | alias Ants.Worlds.Tile.{Land, Home, Rock} 6 | alias Ants.Worlds.TileType 7 | alias Ants.Worlds.TileLookupMock 8 | alias Ants.Worlds.WorldMap 9 | alias Ants.Worlds.Point 10 | alias Ants.Worlds.Surroundings 11 | 12 | describe "surroundings" do 13 | # Global mode so the async tasks can do a lookup 14 | setup :set_mox_global 15 | 16 | test "finds the surroundings in a world" do 17 | world_map = [ 18 | "0 0 0 0 0", 19 | "0 0 _ _ 0", 20 | "0 _ H 0 0", 21 | "0 _ 0 _ 0", 22 | "0 0 0 0 0" 23 | ] 24 | 25 | mock_tile_lookup(world_map) 26 | 27 | assert Surroundings.surroundings(1, 2, 2) == 28 | [ 29 | %Land{}, 30 | %Rock{}, 31 | %Land{}, 32 | %Land{}, 33 | %Home{}, 34 | %Rock{}, 35 | %Rock{}, 36 | %Land{}, 37 | %Land{} 38 | ] 39 | end 40 | end 41 | 42 | defp mock_tile_lookup(world_map) do 43 | tile_types = 44 | world_map 45 | |> WorldMap.to_tile_type_list() 46 | |> Enum.map(fn type -> 47 | {:ok, tile} = TileType.tile_of_type(type) 48 | tile 49 | end) 50 | |> List.to_tuple() 51 | 52 | size = length(world_map) 53 | 54 | TileLookupMock 55 | |> stub(:lookup, fn _, x, y -> 56 | lookup_tile(tile_types, size, x, y) 57 | end) 58 | end 59 | 60 | defp tile_at_index(index, tile_types) do 61 | elem(tile_types, index) 62 | end 63 | 64 | defp lookup_tile(tiles, size, x, y) do 65 | Point.to_index({x, y}, size) 66 | |> tile_at_index(tiles) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/ants/worlds/tile_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ants.Worlds.TileTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Ants.Worlds.Tile 5 | alias Ants.Worlds.Tile.{Land, Food, Rock, Home} 6 | 7 | describe "given a land tile" do 8 | setup [:create_land] 9 | 10 | test "returns a land", %{tile: tile} do 11 | assert Tile.get(tile) == %Land{} 12 | end 13 | 14 | test "adds pheromones", %{tile: tile} do 15 | assert Tile.deposit_pheromones(tile) == {:ok} 16 | assert Tile.get(tile) == %Land{pheromone: 1} 17 | 18 | assert Tile.deposit_pheromones(tile) == {:ok} 19 | assert Tile.get(tile) == %Land{pheromone: 2} 20 | end 21 | 22 | test "can't take food", %{tile: tile} do 23 | cant_take_food(tile) 24 | end 25 | 26 | test "can't deposit food", %{tile: tile} do 27 | cant_deposit_food(tile) 28 | end 29 | end 30 | 31 | describe "given a food tile" do 32 | setup [:create_food] 33 | 34 | test "returns a full food", %{tile: tile} do 35 | assert Tile.get(tile) == %Food{food: 10} 36 | end 37 | 38 | test "can't add pheromones", %{tile: tile} do 39 | cant_deposit_pheromones(tile) 40 | end 41 | 42 | test "can take food", %{tile: tile} do 43 | assert Tile.take_food(tile) == {:ok, 1} 44 | end 45 | 46 | test "converts empty food to land", %{tile: tile} do 47 | Enum.each(1..10, fn _ -> Tile.take_food(tile) end) 48 | 49 | assert Tile.take_food(tile) == {:error, :not_food} 50 | assert Tile.get(tile) == %Land{} 51 | end 52 | end 53 | 54 | describe "given a rock tile" do 55 | setup [:create_rock] 56 | 57 | test "returns a rock", %{tile: tile} do 58 | assert Tile.get(tile) == %Rock{} 59 | end 60 | 61 | test "can't add pheromones", %{tile: tile} do 62 | cant_deposit_pheromones(tile) 63 | end 64 | 65 | test "can't take food", %{tile: tile} do 66 | cant_take_food(tile) 67 | end 68 | 69 | test "can't deposit food", %{tile: tile} do 70 | cant_deposit_food(tile) 71 | end 72 | end 73 | 74 | describe "given a home tile" do 75 | setup [:create_home] 76 | 77 | test "returns a home with no food", %{tile: tile} do 78 | assert Tile.get(tile) == %Home{food: 0} 79 | end 80 | 81 | test "can't add pheromones", %{tile: tile} do 82 | cant_deposit_pheromones(tile) 83 | end 84 | 85 | test "can't take food", %{tile: tile} do 86 | cant_take_food(tile) 87 | end 88 | 89 | test "can deposit food", %{tile: tile} do 90 | assert Tile.deposit_food(tile) == {:ok, 1} 91 | assert Tile.deposit_food(tile) == {:ok, 1} 92 | 93 | assert Tile.get(tile) == %Home{food: 2} 94 | end 95 | end 96 | 97 | defp create_land(_context) do 98 | {:ok, tile} = GenServer.start_link(Tile, :land) 99 | %{tile: tile} 100 | end 101 | 102 | defp create_rock(_context) do 103 | {:ok, tile} = GenServer.start_link(Tile, :rock) 104 | %{tile: tile} 105 | end 106 | 107 | defp create_home(_context) do 108 | {:ok, tile} = GenServer.start_link(Tile, :home) 109 | %{tile: tile} 110 | end 111 | 112 | defp create_food(_context) do 113 | {:ok, tile} = GenServer.start_link(Tile, :light_food) 114 | %{tile: tile} 115 | end 116 | 117 | defp cant_take_food(tile) do 118 | assert Tile.take_food(tile) == {:error, :not_food} 119 | end 120 | 121 | defp cant_deposit_pheromones(tile) do 122 | assert Tile.deposit_pheromones(tile) == {:error, :not_land} 123 | end 124 | 125 | defp cant_deposit_food(tile) do 126 | assert Tile.deposit_food(tile) == {:error, :not_home} 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /test/ants/worlds/tile_type_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ants.Worlds.TileTypeTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Ants.Shared.Knobs 5 | alias Ants.Worlds.Tile.{Land, Food, Rock, Home} 6 | alias Ants.Worlds.TileType 7 | 8 | @starting_food Knobs.constant(:starting_food) 9 | 10 | describe "given a tile type" do 11 | test "returns a land" do 12 | assert TileType.tile_of_type(:land) == {:ok, %Land{}} 13 | end 14 | 15 | test "returns a rock" do 16 | assert TileType.tile_of_type(:rock) == {:ok, %Rock{}} 17 | end 18 | 19 | test "returns a home" do 20 | assert TileType.tile_of_type(:home) == {:ok, %Home{}} 21 | end 22 | 23 | test "returns a food" do 24 | assert TileType.tile_of_type(:food) == {:ok, %Food{food: @starting_food}} 25 | end 26 | 27 | test "returns a light_pheromone" do 28 | assert TileType.tile_of_type(:light_pheromone) == {:ok, %Land{pheromone: 5}} 29 | end 30 | 31 | test "returns a heavy_pheromone" do 32 | assert TileType.tile_of_type(:heavy_pheromone) == {:ok, %Land{pheromone: 10}} 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/ants/worlds/world_test.exs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/ants_web/controllers/app_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.AppControllerTest do 2 | use AntsWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get(conn, "/") 6 | assert html_response(conn, 200) =~ "Ants" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/ants_web/views/app_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.PageViewTest do 2 | use AntsWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /test/ants_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.ErrorViewTest do 2 | use AntsWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "render 500.html" do 8 | assert render_to_string(AntsWeb.ErrorView, "500.html", []) == "Internal server error" 9 | end 10 | 11 | test "render any other" do 12 | assert render_to_string(AntsWeb.ErrorView, "505.html", []) == "Internal server error" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/ants_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.LayoutViewTest do 2 | use AntsWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | # The default endpoint for testing 24 | @endpoint AntsWeb.Endpoint 25 | end 26 | end 27 | 28 | setup _tags do 29 | :ok 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule AntsWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | import AntsWeb.Router.Helpers 23 | 24 | # The default endpoint for testing 25 | @endpoint AntsWeb.Endpoint 26 | end 27 | end 28 | 29 | setup _tags do 30 | {:ok, conn: Phoenix.ConnTest.build_conn()} 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/support/mocks.ex: -------------------------------------------------------------------------------- 1 | alias Ants.Worlds 2 | alias Ants.WorldsMock 3 | 4 | alias Ants.Worlds.TileSupervisor 5 | alias Ants.Worlds.TileSupervisorMock 6 | 7 | alias Ants.Worlds.TileLookup 8 | alias Ants.Worlds.TileLookupMock 9 | 10 | Mox.defmock(WorldsMock, for: Worlds) 11 | Mox.defmock(TileLookupMock, for: TileLookup) 12 | Mox.defmock(TileSupervisorMock, for: TileSupervisor) 13 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------