├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cljs-test └── brute │ └── test_runner.cljs ├── dev ├── Dockerfile ├── profiles.clj ├── startup.sh ├── user.clj └── zshrc ├── project.clj ├── src └── brute │ ├── entity.cljc │ └── system.cljc ├── test └── brute │ ├── entity_test.cljc │ └── system_test.cljc └── wercker.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | .* 9 | !.gitignore 10 | *.iml 11 | /doc/codox 12 | /node_modules 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of Washington and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile for managing the Docker dev environment and 3 | # the resources within it. 4 | # 5 | 6 | # __ __ _ _ _ 7 | # \ \ / /_ _ _ __(_) __ _| |__ | | ___ ___ 8 | # \ \ / / _` | '__| |/ _` | '_ \| |/ _ \ __| 9 | # \ V / (_| | | | | (_| | |_) | | __\__ \ 10 | # \_/ \__,_|_| |_|\__,_|_.__/|_|\___|___/ 11 | # 12 | 13 | TAG=markmandel/brute-dev 14 | NAME=brute-dev 15 | 16 | #Directory that this Makefile is in. 17 | mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) 18 | current_path := $(dir $(mkfile_path)) 19 | 20 | # _____ _ 21 | # |_ _|_ _ _ __ __ _ ___| |_ ___ 22 | # | |/ _` | '__/ _` |/ _ \ __/ __| 23 | # | | (_| | | | (_| | __/ |_\__ \ 24 | # |_|\__,_|_| \__, |\___|\__|___/ 25 | # |___/ 26 | 27 | # build the docker dev image 28 | build: 29 | docker build --tag=$(TAG) $(current_path)/dev 30 | 31 | #clean up the shell 32 | clean: 33 | docker rmi $(TAG) 34 | 35 | # Start a development shell 36 | shell: m2 37 | mkdir -p ~/.gnupg 38 | docker run --rm \ 39 | --name=$(NAME) \ 40 | -P=true \ 41 | -e HOST_GID=`id -g` \ 42 | -e HOST_UID=`id -u` \ 43 | -e HOST_USER=$(USER) \ 44 | -e DOCKER_GID=$(word 3,$(subst :, ,$(shell getent group docker))) \ 45 | -v ~/.m2:/home/$(USER)/.m2 \ 46 | -v ~/.gnupg:/home/$(USER)/.gnupg \ 47 | -v $(current_path)/dev/profiles.clj:/home/$(USER)/.lein/profiles.clj \ 48 | -v $(current_path)/dev/zshrc:/home/$(USER)/.zshrc \ 49 | -v $(current_path):/project \ 50 | -v /usr/bin/docker:/usr/bin/docker \ 51 | -v /var/run/docker.sock:/var/run/docker.sock \ 52 | -it $(TAG) /root/startup.sh 53 | 54 | shell-attach: 55 | docker exec -it --user=$(USER) $(NAME) zsh 56 | 57 | # mount the docker's jvm in the /tmp dir 58 | shell-mount-jvm: 59 | mkdir -p /tmp/$(NAME)/jvm 60 | sshfs $(USER)@0.0.0.0:/usr/lib/jvm /tmp/$(NAME)/jvm -p $(call getPort,22) -o follow_symlinks 61 | 62 | # Run the tests inside the docker container, and output the results. 63 | test: m2 64 | docker run --rm \ 65 | -v ~/.m2:/root/.m2 \ 66 | -v $(current_path):/project \ 67 | $(TAG) lein alltest 68 | 69 | # make sure the maven local dir is there 70 | m2: 71 | mkdir -p ~/.m2 72 | 73 | # push the image up to docker hub 74 | push: 75 | docker push $(TAG) 76 | 77 | # ____ _ _ _ _____ _ 78 | # / ___|| |__ ___| | | |_ _|_ _ _ __ __ _ ___| |_ ___ 79 | # \___ \| '_ \ / _ \ | | | |/ _` | '__/ _` |/ _ \ __/ __| 80 | # ___) | | | | __/ | | | | (_| | | | (_| | __/ |_\__ \ 81 | # |____/|_| |_|\___|_|_| |_|\__,_|_| \__, |\___|\__|___/ 82 | # |___/ 83 | 84 | wercker-build: 85 | wercker --verbose --debug build --docker-local --direct-mount --working-dir /tmp 86 | 87 | # _____ _ _ 88 | # | ___| _ _ __ ___| |_(_) ___ _ __ ___ 89 | # | |_ | | | | '_ \ / __| __| |/ _ \| '_ \/ __| 90 | # | _|| |_| | | | | (__| |_| | (_) | | | \__ \ 91 | # |_| \__,_|_| |_|\___|\__|_|\___/|_| |_|___/ 92 | # 93 | 94 | # get the mapped docker host port 95 | getPort = $(word 2,$(subst :, ,$(shell docker port $(NAME) $(1)))) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brute 2 | 3 | A simple and lightweight Entity Component System library for writing games with Clojure or ClojureScript. 4 | 5 | ![Clojars Version](https://clojars.org/brute/latest-version.svg?v=3) 6 | 7 | [![wercker status](https://app.wercker.com/status/5f5d692036ee110c41a50ccc7b6f4ae5/m "wercker status")](https://app.wercker.com/project/bykey/5f5d692036ee110c41a50ccc7b6f4ae5) 8 | 9 | The aim of this project was to use basic Clojure building blocks to form an Entity System architecture, and get out of the 10 | author's way when deciding exactly what approach would best fit their game when integrating with this library. 11 | 12 | To that end: 13 | 14 | - Entities are UUIDs. 15 | - The Component type system can be easily extended through a multimethod `get-component-type`, but defaults to using the component's instance class as its type. 16 | - Components can therefore be defrecords or deftypes by default, but could easily be maps or just about anything else. 17 | - Systems are simply references to functions of the format `(fn [delta])`. 18 | 19 | To learn more about Entity Component Systems, please read the [Entity Systems Wiki](http://entity-systems.wikidot.com/). 20 | I personally, also found [Adam Martin's Blog Post series](http://t-machine.org/index.php/2007/09/03/entity-systems-are-the-future-of-mmog-development-part-1/) 21 | very useful at giving a step by step explanation of Entity System architecture. 22 | 23 | ## News 24 | 25 | Blog posts and news can be found on the [Compound Theory Blog](http://www.compoundtheory.com/category/brute) 26 | 27 | ## Usage 28 | 29 | See the [Library API](https://markmandel.github.io/brute/codox/) for all the functionality of this library. 30 | 31 | ### Quick Start 32 | 33 | A quick example based overview of what functionality Brute provides. 34 | 35 | I've used fully qualified namespace, *brute.entity* and *brute.system* to be explicit about what is part of Brute in the demo code below, and what denotes custom code. 36 | 37 | #### Creating the Basic Entity Component System 38 | 39 | Brute doesn't store any data in a ref/atom, but instead provides you with the functions and capabilities for manipulating an immutable data structure that represents this ES system. This is particularly useful because: 40 | 41 | - How the entity data structure is persisted is up to you and the library you are using (although 9/10 times I expect it will end up stored in a single atom, and reset! on each game loop), which gives you complete control over when state mutation occurs – if it occurs at all. This makes concurrent processes much simpler to develop. 42 | - You get direct access to the ES data structure, in case you want to do something with it that isn’t exposed in the current API. 43 | - You can easily have multiple ES systems within a single game, e.g. for sub-games. 44 | - Saving a game becomes simple: Just serialise the ES data structure and store. Deserialise to load. 45 | - Basically all the good stuff having immutable data structures and pure functions should give you. 46 | 47 | To create the initial system data structure: 48 | 49 | ```clojure 50 | (brute.entity/create-system) 51 | ``` 52 | 53 | This is actually a map, that lets you access Entities and their Components from a variety of ways, so you can always do it in a performant way. 54 | 55 | ```clojure 56 | {;; Nested Map of Component Types -> Entity -> Component Instance 57 | :entity-components {} 58 | ;; Map of Entities -> Set of Component Types 59 | :entity-component-types {}} 60 | ``` 61 | 62 | Do note, that this data structure may be subject to change between releases. 63 | 64 | #### Creating a Ball Entity, with corresponding Component instances. 65 | 66 | - A `Ball` component instance to know it is a Ball. 67 | - A `Rectangle` component instance to draw a rectangle in its' place 68 | - A `Velocity` component instance to know what direction it is travelling in, and how fast. 69 | 70 | ```clojure 71 | (defn create-ball 72 | "Creates a ball entity" 73 | [system] 74 | (let [ball (brute.entity/create-entity) ;; Returns a UUID for the Entity 75 | center-x (-> (graphics! :get-width) (/ 2) (m/round)) 76 | center-y (-> (graphics! :get-height) (/ 2) (m/round)) 77 | ball-size 20 78 | ball-center-x (- center-x (/ ball-size 2)) 79 | ball-center-y (- center-y (/ ball-size 2)) 80 | angle (create-random-angle)] 81 | (-> system 82 | (brute.entity/add-entity ball) ;; Adds the entity to the ES data structure and returns it 83 | (brute.entity/add-component ball (c/->Ball)) ;; Adds the Ball instance to the ES data structure and returns it 84 | (brute.entity/add-component ball (c/->Rectangle (rectangle ball-center-x ball-center-y ball-size ball-size) (color :white))) ;; Adds the Rectangle instance to the ES data structure and returns it 85 | (brute.entity/add-component ball (c/->Velocity (vector-2 0 300 :set-angle angle)))))) ;; Adds the Velocity instance to the ES data structure and returns it 86 | ``` 87 | 88 | #### Render each of the Entities that have a Rectangle Component 89 | 90 | ```clojure 91 | (defn- render-rectangles 92 | "Render all the rectangles" 93 | [system] 94 | (let [shape-renderer (:shape-renderer (:renderer system))] 95 | (.begin shape-renderer ShapeRenderer$ShapeType/Filled) 96 | (doseq [entity (brute.entity/get-all-entities-with-component system Rectangle)] ;; loop around all the entities that have a Rectangle Component instance 97 | (let [rect (brute.entity/get-component system entity Rectangle) ;; get the Rectangle Component Instance for this entity 98 | geom (:rect rect)] ;; Rectangle component contains a Rectangle geometry shape. 99 | (doto shape-renderer ;; Draw the actual rectangle on the screen 100 | (.setColor (:colour rect)) ;; Rectangle component contains the colour 101 | (.rect (rectangle! geom :get-x) 102 | (rectangle! geom :get-y) 103 | (rectangle! geom :get-width) 104 | (rectangle! geom :get-height))))) 105 | (.end shape-renderer))) 106 | ``` 107 | 108 | #### Systems Management 109 | System management is an optional feature for you to use with Brute. 110 | 111 | The following adds each system function to a list contains on the Entity System data structure, maintaining the order in which they were added. 112 | 113 | ```clojure 114 | (defn- create-systems 115 | "register all the system functions" 116 | [system] 117 | (-> system 118 | (brute.system/add-system-fn input/process-one-game-tick) 119 | (brute.system/add-system-fn scoring/process-one-game-tick) 120 | (brute.system/add-system-fn ai/process-one-game-tick) 121 | (brute.system/add-system-fn physics/process-one-game-tick) 122 | (brute.system/add-system-fn rendering/process-one-game-tick))) 123 | ``` 124 | 125 | Finally call each function in the order added, simply write: 126 | 127 | ```clojure 128 | (brute.system/process-one-game-tick system (graphics! :get-delta-time)) 129 | ``` 130 | 131 | 132 | 133 | ## Game Examples 134 | 135 | - [Pong Clone](https://github.com/markmandel/brute-play-pong) written with [play-clj](https://github.com/oakes/play-clj) 136 | 137 | ## Contributing 138 | 139 | Pull requests are always welcome! 140 | 141 | Active development happens on the `develop` branch. The `master` branch is the source for the current release. 142 | 143 | ### Reader Conditionals 144 | This project uses [Reader Conditionals](http://clojure.org/reader#The%20Reader--Reader%20Conditionals) to support both Clojure and ClojureScript. It should be a seamless experience. 145 | 146 | ## Testing 147 | 148 | To test under Clojure: `lein test` 149 | 150 | To test under ClojureScript: `lein cljstest` 151 | 152 | To run all tests: `lein alltest` 153 | 154 | ### Run all tests in the a Docker Container 155 | You should be able to run all the tests without having to install anything, except to pull the Docker container. 156 | 157 | `make test` will run all the tests in the development Docker container, which should make development easier. 158 | 159 | ## License 160 | 161 | Copyright © 2016 Mark Mandel, Google Inc. 162 | 163 | Distributed under the Eclipse Public License either version 1.0 or (at 164 | your option) any later version. 165 | -------------------------------------------------------------------------------- /cljs-test/brute/test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns brute.test-runner 2 | "the test runner" 3 | (:require [doo.runner :refer-macros [doo-tests]] 4 | [brute.entity-test] 5 | [brute.system-test])) 6 | 7 | (doo-tests 'brute.entity-test 8 | 'brute.system-test) 9 | 10 | -------------------------------------------------------------------------------- /dev/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # markmandel/brute-dev 3 | # 4 | 5 | FROM java:jdk 6 | 7 | RUN apt-get update && \ 8 | apt-get install -y zsh openssh-server libapparmor1 git gnupg 9 | 10 | #sshd setup - https://docs.docker.com/examples/running_ssh_service/ 11 | RUN mkdir /var/run/sshd 12 | RUN echo 'root:pw' | chpasswd 13 | RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config 14 | # SSH login fix. Otherwise user is kicked off after login 15 | RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd 16 | ENV NOTVISIBLE "in users profile" 17 | RUN echo "export VISIBLE=now" >> /etc/profile 18 | EXPOSE 22 19 | 20 | #oh-my-zsh, because how do we live without it? 21 | RUN git clone https://github.com/robbyrussell/oh-my-zsh.git 22 | 23 | #lein installation 24 | ENV LEIN_ROOT=1 25 | RUN cd /usr/local/bin/ && wget https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein && chmod +x ./lein 26 | 27 | RUN lein 28 | 29 | #Node installation 30 | RUN curl -sL https://deb.nodesource.com/setup_4.x | bash - 31 | RUN apt-get install -y nodejs 32 | 33 | #wecker tools installation 34 | RUN curl https://s3.amazonaws.com/downloads.wercker.com/cli/stable/linux_amd64/wercker -o /usr/local/bin/wercker 35 | RUN chmod +x /usr/local/bin/wercker 36 | 37 | ADD startup.sh /root/startup.sh 38 | RUN chmod +x /root/startup.sh 39 | 40 | RUN mkdir /project 41 | WORKDIR /project -------------------------------------------------------------------------------- /dev/profiles.clj: -------------------------------------------------------------------------------- 1 | {:user {:plugins [[lein-ancient "0.6.8"]]}} -------------------------------------------------------------------------------- /dev/startup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | groupadd --gid $HOST_GID $HOST_USER 4 | useradd $HOST_USER --home /home/$HOST_USER --gid $HOST_GID --uid $HOST_UID --shell /usr/bin/zsh 5 | echo "$HOST_USER:pw" | chpasswd 6 | 7 | #zsh 8 | chown -R $HOST_USER:$HOST_USER /oh-my-zsh 9 | 10 | #setup lein for this user 11 | cp -r /root/.lein/* /home/$HOST_USER/.lein/ 12 | 13 | #make sure all permissions are good to go. 14 | chown -R $HOST_USER:$HOST_USER /home/$HOST_USER 15 | 16 | #allow docker passthrough 17 | groupadd --gid $DOCKER_GID docker 18 | usermod -a -G docker $HOST_USER 19 | 20 | /usr/sbin/sshd 21 | su $HOST_USER -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | "Tools for interactive development with the REPL. This file should 3 | not be included in a production build of the application." 4 | (:use [clojure.repl] 5 | [clojure.tools.namespace.repl :only (refresh refresh-all set-refresh-dirs)]) 6 | (:require [brute.entity :as es] 7 | [clojure.test :refer (run-tests run-all-tests)])) 8 | 9 | ;; system init functions 10 | (def system 11 | "A Var containing an object representing the application under 12 | development." 13 | nil) 14 | 15 | (defn create 16 | "Creates and initializes the system under development in the Var 17 | #'system." 18 | [] 19 | (alter-var-root #'system (constantly (es/create-system)))) 20 | 21 | (defn go 22 | "Initializes and starts the system running." 23 | [] 24 | (create) 25 | :ready) 26 | 27 | (defn reset 28 | "Stops the system, optionally reloads modified source files, and restarts it." 29 | [] 30 | (refresh :after 'user/go)) -------------------------------------------------------------------------------- /dev/zshrc: -------------------------------------------------------------------------------- 1 | # Path to your oh-my-zsh configuration. 2 | ZSH=/oh-my-zsh 3 | 4 | # Theme 5 | ZSH_THEME="lambda" 6 | 7 | plugins=(lein debian git git-extras colored-man zsh_reload dirhistory) 8 | 9 | source $ZSH/oh-my-zsh.sh 10 | 11 | #yes, I suck. 12 | export EDITOR='nano -w' -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject brute "0.4.0" 2 | :description "A simple and lightweight Entity Component System library for writing games with Clojure" 3 | :url "http://www.github.com/markmandel/brute" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.7.0"] 7 | [org.clojure/clojurescript "1.7.170"] 8 | [org.clojure/math.numeric-tower "0.0.4"]] 9 | :plugins [[lein-codox "0.9.0"] 10 | [lein-cljsbuild "1.1.1"] 11 | [lein-doo "0.1.6"]] 12 | :cljsbuild {:builds {:src {:source-paths ["src"] 13 | :compiler {:output-to "target/js/src/brute.js" 14 | :output-dir "target/js/src" 15 | :source-map true 16 | :optimizations :none 17 | :pretty-print true}} 18 | :test {:source-paths ["src" "test" "cljs-test"] 19 | :compiler {:output-to "target/js/test/brute.js" 20 | :output-dir "target/js/test" 21 | :source-map true 22 | :main 'brute.test-runner 23 | :optimizations :none 24 | :pretty-print true 25 | :target :nodejs}}}} 26 | :aliases {"alltest" ["do" "clean" ["test"] ["doo" "node" "test" "once"] "clean"] 27 | "cljstest" ["doo" "node" "test"]} 28 | :profiles {:dev {:dependencies [[org.clojure/tools.namespace "0.2.11"]] 29 | :source-paths ["dev"] 30 | :repl-options {:init-ns user} 31 | :codox {:source-paths ["src"] :namespaces [brute.system brute.entity]}}}) -------------------------------------------------------------------------------- /src/brute/entity.cljc: -------------------------------------------------------------------------------- 1 | (ns brute.entity 2 | "Entity Manager functions for the Brute Entity Component System") 3 | 4 | (defn create-system 5 | "Creates the system data structure that will need to be passed to all entity functions" 6 | [] 7 | {;; Nested Map of Component Types -> Entity -> Component Instance 8 | :entity-components {} 9 | ;; Map of Entities -> Set of Component Types 10 | :entity-component-types {}}) 11 | 12 | (defn create-uuid 13 | "create a UUID" 14 | [] 15 | #?(:clj (java.util.UUID/randomUUID) 16 | :cljs (random-uuid))) 17 | 18 | (defn create-entity 19 | "Create the entity and return it. Entities are just UUIDs" 20 | [] 21 | (create-uuid)) 22 | 23 | (defn get-all-entities 24 | "Returns a list of all the entities. Not that useful in application, but good for debugging/testing" 25 | [system] 26 | (if-let [result (-> system :entity-component-types keys)] 27 | result 28 | [])) 29 | 30 | (defn add-entity 31 | "Add the entity to the ES Data Structure and returns it" 32 | [system entity] 33 | (let [system (transient system)] 34 | (-> system 35 | (assoc! :entity-component-types (-> system :entity-component-types (assoc entity #{}))) 36 | persistent!))) 37 | 38 | (defmulti get-component-type 39 | "Returns the type for a given component. Using a multimethod with 'class' as the dispatch-fn to allow for extensibility per application. 40 | By default returns the class of the component." 41 | #?(:clj class 42 | :cljs type)) 43 | 44 | (defmethod get-component-type :default 45 | [component] 46 | (#?(:clj class 47 | :cljs type) component)) 48 | 49 | (defn add-component 50 | "Add a component instance to a given entity in the ES data structure and returns it. 51 | Will overwrite a component if already set." 52 | [system entity instance] 53 | (let [type (get-component-type instance) 54 | system (transient system) 55 | ecs (:entity-components system) 56 | ects (:entity-component-types system)] 57 | (-> system 58 | (assoc! :entity-components (assoc-in ecs [type entity] instance)) 59 | (assoc! :entity-component-types (assoc ects entity (-> ects (get entity) (conj type)))) 60 | persistent!))) 61 | 62 | (defn get-component 63 | "Get the component data for a specific component type" 64 | [system entity type] 65 | (-> system :entity-components (get-in [type entity]))) 66 | 67 | (defn update-component 68 | "Update an entity's component instance through through fn. Function is applied first with the specified component and any other args applied, 69 | and should return the modified component instance. Return nil if you want no change to occur." 70 | [system entity type fn & args] 71 | (if-let [update (apply fn (get-component system entity type) args)] 72 | (add-component system entity update) 73 | system)) 74 | 75 | (defn get-all-entities-with-component 76 | "Get all the entities that have a given component type" 77 | [system type] 78 | (if-let [entities (-> system :entity-components (get type) keys)] 79 | entities 80 | [])) 81 | 82 | (defn remove-component 83 | "Remove a component instance from the ES data structure and returns it" 84 | [system entity instance] 85 | (let [type (get-component-type instance) 86 | system (transient system) 87 | entity-components (:entity-components system) 88 | entity-component-types (:entity-component-types system)] 89 | (-> system 90 | (assoc! :entity-components (assoc entity-components type (-> entity-components (get type) (dissoc entity)))) 91 | (assoc! :entity-component-types (assoc entity-component-types entity (-> entity-component-types (get entity) (disj type)))) 92 | persistent!))) 93 | 94 | (defn kill-entity 95 | "Destroy an entity completely from the ES data structure and returns it" 96 | [system entity] 97 | (let [system (transient system) 98 | entity-component-types (:entity-component-types system)] 99 | (-> system 100 | (assoc! :entity-component-types (dissoc entity-component-types entity)) 101 | (assoc! :entity-components (persistent! (reduce (fn [v type] (assoc! v type (dissoc (get v type) entity))) 102 | (transient (:entity-components system)) (get entity-component-types entity)))) 103 | persistent!))) 104 | 105 | (defn get-all-components-on-entity 106 | "Get all the components on a specific entity. Useful for debugging" 107 | [system entity] 108 | (map #(get-in (:entity-components system) [% entity]) (get (:entity-component-types system) entity))) 109 | -------------------------------------------------------------------------------- /src/brute/system.cljc: -------------------------------------------------------------------------------- 1 | (ns brute.system 2 | "Utility functions for system management. 3 | Systems in brute are simply functions that manage aspects like Physics, Rendering, Camera etc" 4 | #?(:clj 5 | (:require [clojure.math.numeric-tower :as m]))) 6 | 7 | (defn add-system-fn 8 | "Add a function that represents a system, e.g. Physics, Rendering, etc. 9 | This needs to be in the structure: (fn [system delta]) where 'delta' is the number of milliseconds 10 | since the last game tick. This will also need to return the system in the state you want passed to the 11 | next system-fn, and ultimately out of process-one-game-tick. 12 | This will then be called directly when `process-one-game-tick` is called" 13 | [system system-fn] 14 | (assoc system :system-fns (conj (:system-fns system) system-fn))) 15 | 16 | (defn- throttled-fn 17 | "The function that does the actual throttling." 18 | [system-fn atom threshhold system delta] 19 | (swap! atom + delta) 20 | (if (>= @atom threshhold) 21 | (reduce (fn [v _] ;; this takes care of when the framerate 22 | (swap! atom - threshhold) ;; is WAY slower than the throttle. 23 | (system-fn v delta)) 24 | system (-> @atom (/ threshhold) #?(:clj m/floor :cljs Math/floor) range)) 25 | system)) 26 | 27 | (defn add-throttled-system-fn 28 | "Same as `add-system-fn`, but will only execute the `system-fn` after `threshold` milliseconds has been equalled or passed." 29 | [system system-fn threshold] 30 | (add-system-fn system (partial throttled-fn system-fn (atom 0) threshold))) 31 | 32 | (defn process-one-game-tick 33 | "Optional convenience function that calls each of the system functions that have been added in turn, with the provided delta." 34 | [system delta] 35 | (reduce (fn [sys sys-fn] (sys-fn sys delta)) 36 | system (:system-fns system))) 37 | -------------------------------------------------------------------------------- /test/brute/entity_test.cljc: -------------------------------------------------------------------------------- 1 | (ns brute.entity-test 2 | "Tests for the entity management functions" 3 | #?(:clj 4 | (:import (java.util UUID) 5 | (clojure.lang PersistentArrayMap))) 6 | (:require [brute.entity :refer [create-system 7 | create-entity 8 | add-entity 9 | get-all-entities 10 | get-component-type 11 | add-component 12 | get-component 13 | update-component 14 | get-all-entities-with-component 15 | remove-component 16 | kill-entity 17 | get-all-components-on-entity]] 18 | #?(:clj 19 | [clojure.test :refer :all] 20 | :cljs [cljs.test :refer-macros [deftest is use-fixtures]]))) 21 | 22 | (def system (atom 0)) 23 | (defrecord Position [x y]) 24 | (defrecord Velocity [x y]) 25 | 26 | (defn- setup! 27 | "Provides setup for the tests. Has side effects" 28 | [f] 29 | (reset! system (create-system)) 30 | (f)) 31 | 32 | (defn- r! [s] (reset! system s)) 33 | 34 | (use-fixtures :each setup!) 35 | 36 | (defmethod get-component-type PersistentArrayMap 37 | [component] 38 | (:type component)) 39 | 40 | ;; The Entity I create is a unique uuid 41 | (deftest entity-unique-uuid 42 | (let [uuid (create-entity)] 43 | (is uuid) 44 | #?(:clj (do (is (= (-> uuid .toString .length) 36)) 45 | (is (= (class uuid) UUID))) 46 | :cljs (do (is (= (-> uuid .toString .-length) 36)) 47 | (is (= (type uuid) UUID)))) 48 | (is (not= (create-entity) uuid)))) 49 | 50 | ;; Creating and adding an entity results in it being added to the global list 51 | (deftest global-entity-list 52 | (let [entity (create-entity)] 53 | (is (= (-> @system 54 | (add-entity entity) 55 | (get-all-entities)) [entity])))) 56 | 57 | ;; By default, a component returns it's class as it's type 58 | (deftest default-class-as-component-type 59 | (let [pos (->Position 5 5)] 60 | (is (= (get-component-type pos) (#?(:clj class :cljs type) pos))))) 61 | 62 | 63 | ;; We can extend the component type system, through the multimethod 64 | (deftest extend-component-type-multimethod 65 | (let [pos {:type :position :x 5 :y 5}] 66 | (is (= (get-component-type pos) :position)))) 67 | 68 | ;; You can add a component instance to an entity, and then retrieve it again 69 | (deftest add-then-get-component 70 | (let [entity (create-entity) 71 | pos (->Position 5 5)] 72 | (is (= (-> @system 73 | (add-entity entity) 74 | (add-component entity pos) 75 | (get-component entity Position)) pos)))) 76 | 77 | 78 | ;; You can add a component instance to an entity, and then overwrite it with another component of the same type 79 | (deftest overwrite-component-of-same-type 80 | (let [entity (create-entity) 81 | pos (->Position 5 5) 82 | pos2 (->Position 10 10)] 83 | (is (= (-> @system 84 | (add-entity entity) 85 | (add-component entity pos) 86 | r! 87 | (get-component entity Position)) pos)) 88 | (is (= (-> @system 89 | (add-component entity pos2) 90 | (get-component entity Position)) pos2)))) 91 | 92 | ;; You can add an extended component instance to an entity, and then retrieve it again 93 | (deftest add-then-get-extended-component 94 | (let [entity (create-entity) 95 | pos {:type :position :x 5 :y 5}] 96 | (is (= (-> @system 97 | (add-entity entity) 98 | (add-component entity pos) 99 | (get-component entity :position)) pos)))) 100 | 101 | ;; If an entity doesn't have a component, it should return nil 102 | (deftest entity-without-component-nil 103 | (let [entity (create-entity) 104 | pos (->Position 5 5)] 105 | (is (nil? (-> @system 106 | (add-entity entity) 107 | (get-component entity Position)))) 108 | 109 | (is (nil? (-> @system 110 | (add-component entity pos) 111 | (get-component entity Velocity)))))) 112 | 113 | ;; Can retrieve all entites that have a single type 114 | (deftest add-component-get-all-entities 115 | (is (= (get-all-entities-with-component @system Position))) 116 | 117 | (let [entity1 (create-entity) 118 | entity2 (create-entity) 119 | pos (->Position 5 5)] 120 | 121 | 122 | (-> @system 123 | (add-entity entity1) 124 | (add-entity entity2) 125 | (add-component entity1 pos) 126 | (add-component entity2 pos) 127 | r!) 128 | 129 | (is (= (frequencies (get-all-entities-with-component @system Position)) 130 | (frequencies [entity1, entity2]))))) 131 | 132 | (deftest add-component-get-all-extended-entities 133 | (is (= (get-all-entities-with-component @system :position))) 134 | 135 | (let [entity1 (create-entity) 136 | entity2 (create-entity) 137 | pos {:type :position :x 5 :y 5}] 138 | 139 | (-> @system 140 | (add-entity entity1) 141 | (add-entity entity2) 142 | (add-component entity1 pos) 143 | (add-component entity2 pos) 144 | r!) 145 | 146 | (is (= (frequencies (get-all-entities-with-component @system :position)) 147 | (frequencies [entity1, entity2]))))) 148 | 149 | ;; Are able to removing an entity's component 150 | 151 | (deftest remove-entity-component 152 | (let [entity (create-entity) 153 | pos (->Position 5 5) 154 | vel (->Velocity 10 10)] 155 | (-> @system 156 | (add-entity entity) 157 | (add-component entity pos) 158 | (add-component entity vel) 159 | r!) 160 | 161 | (is (get-component @system entity Position)) 162 | (is (get-component @system entity Velocity)) 163 | (is (= (get-all-entities-with-component @system Position) [entity])) 164 | (is (= (get-all-entities-with-component @system Velocity) [entity])) 165 | 166 | (-> @system (remove-component entity pos) r!) 167 | 168 | (is (nil? (get-component @system entity Position))) 169 | (is (get-component @system entity Velocity)) 170 | (is (= (get-all-entities-with-component @system Position) [])) 171 | (is (= (get-all-entities-with-component @system Velocity) [entity])) 172 | 173 | (-> @system (remove-component entity vel) r!) 174 | 175 | (is (nil? (get-component @system entity Position))) 176 | (is (nil? (get-component @system entity Velocity))) 177 | (is (empty? (get-all-entities-with-component @system Position))) 178 | (is (empty? (get-all-entities-with-component @system Velocity))))) 179 | 180 | ;; You can kill an entity, and it goes bye bye 181 | (deftest kill-entity-goes-bye-bye 182 | (let [entity (create-entity) 183 | pos (->Position 5 5) 184 | vel (->Velocity 10 10)] 185 | 186 | (is (= (-> @system 187 | (add-entity entity) 188 | (add-component entity pos) 189 | (add-component entity vel) 190 | r! 191 | (get-all-entities)) [entity])) 192 | 193 | (-> @system 194 | (kill-entity entity) 195 | r!) 196 | 197 | (is (empty? (get-all-entities @system))) 198 | (is (nil? (get-component @system entity Position))) 199 | (is (nil? (get-component @system entity Velocity)))) 200 | ) 201 | 202 | ;; You can get all the components on a single entity, if you so choose 203 | (deftest get-all-the-components-on-an-entity 204 | (let [entity (create-entity) 205 | pos (->Position 5 5) 206 | vel (->Velocity 10 10)] 207 | (is (empty? (-> @system 208 | (add-entity entity) 209 | r! 210 | (get-all-components-on-entity entity)))) 211 | (is (= (-> @system 212 | (add-component entity pos) 213 | r! 214 | (get-all-components-on-entity entity)) [pos])) 215 | 216 | (-> @system 217 | (add-component entity vel) 218 | r!) 219 | (is (= (frequencies (get-all-components-on-entity @system entity)) (frequencies [pos vel]))) 220 | 221 | (is (empty? 222 | (-> @system 223 | (kill-entity entity) 224 | (get-all-components-on-entity entity)))))) 225 | 226 | ;; You can update a component by applying a function and parameters to it, like update-in does 227 | (deftest update-component-with-fn 228 | (let [entity (create-entity) 229 | pos (->Position 5 5)] 230 | (-> @system 231 | (add-entity entity) 232 | (add-component entity pos) 233 | r!) 234 | (is (= (:x (get-component @system entity Position)) 5)) 235 | 236 | (-> @system 237 | (update-component entity Position assoc :x 10) 238 | r!) 239 | (is (= (:x (get-component @system entity Position)) 10)) 240 | 241 | ;; send the same thing again, should be the same on the other side 242 | (-> @system 243 | (update-component entity Position assoc :x 10) 244 | r!) 245 | (is (= (:x (get-component @system entity Position)) 10)) 246 | 247 | (-> @system 248 | (update-component entity Position (fn [_] nil)) 249 | r!) 250 | 251 | (is (= (:x (get-component @system entity Position)) 10)))) 252 | -------------------------------------------------------------------------------- /test/brute/system_test.cljc: -------------------------------------------------------------------------------- 1 | (ns brute.system-test 2 | "Tests for the system namespace" 3 | (:require [brute.entity :refer [create-system get-all-entities add-entity create-entity]] 4 | [brute.system :refer [add-system-fn 5 | add-throttled-system-fn 6 | process-one-game-tick]] 7 | #?(:clj 8 | [clojure.test :refer :all] 9 | :cljs [cljs.test :refer-macros [deftest is use-fixtures]]))) 10 | 11 | (def system (atom 0)) 12 | (defrecord Position [x y]) 13 | (defrecord Velocity [x y]) 14 | 15 | (defn- setup! 16 | "Provides setup for the tests. Has side effects" 17 | [f] 18 | (reset! system (create-system)) 19 | (f)) 20 | 21 | (defn- r! [s] (reset! system s)) 22 | 23 | (use-fixtures :each setup!) 24 | 25 | ;; You can add system functions, and then call them per game tick 26 | (deftest add-system-function 27 | (let [counter (atom 0) 28 | sys-fn (fn [sys _] (swap! counter inc) sys)] 29 | 30 | (process-one-game-tick @system 10) 31 | (is (= @counter 0)) 32 | 33 | (-> @system (add-system-fn sys-fn) r!) 34 | 35 | (process-one-game-tick @system 10) 36 | (is (= @counter 1)) 37 | 38 | (-> @system (add-system-fn sys-fn) r!) 39 | 40 | (process-one-game-tick @system 10) 41 | (is (= @counter 3)))) 42 | 43 | ;; Each system function will pass through the system ES data structure 44 | (deftest pass-through-system 45 | (let [sys-fn (fn [system _] system) 46 | e (create-entity)] 47 | 48 | (is (= (-> @system 49 | (add-system-fn sys-fn) 50 | (add-entity e) 51 | (process-one-game-tick 0) 52 | (get-all-entities)) [e])))) 53 | 54 | ;; Calling a throttled function will only fire on every throttling call 55 | (deftest throttled-function 56 | (let [counter (atom 0) 57 | threshold (/ 1000 60) 58 | sys-fn (fn [sys _] (swap! counter inc) sys)] 59 | 60 | (-> @system (add-throttled-system-fn sys-fn threshold) r!) 61 | 62 | (process-one-game-tick @system 0) 63 | (is (= @counter 0)) 64 | 65 | (process-one-game-tick @system 10) 66 | (is (= @counter 0)) 67 | 68 | (process-one-game-tick @system 7) 69 | (process-one-game-tick @system 7) 70 | (is (= @counter 1)) 71 | 72 | (process-one-game-tick @system 0) 73 | (is (= @counter 1)) 74 | 75 | (process-one-game-tick @system 35) 76 | (is (= @counter 3)))) 77 | 78 | ;; Each throttled function will pass through the system ES data structure 79 | (deftest throttled-fn-pass-through-system 80 | (let [counter (atom 0) 81 | threshold (/ 1000 60) 82 | sys-fn (fn [es _] (swap! counter inc) es) 83 | e (create-entity)] 84 | 85 | (-> @system 86 | (add-throttled-system-fn sys-fn threshold) 87 | (add-entity e) 88 | (process-one-game-tick 30) 89 | r!) 90 | 91 | (is (= @counter 1)) 92 | (is (= (get-all-entities @system) [e])))) -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: markmandel/brute-dev 2 | build: 3 | steps: 4 | - script: 5 | name: Run all tests 6 | code: lein alltest --------------------------------------------------------------------------------