https://www.juxt.pro/blog/repl-driven-minecraft [m] Juxt logo AboutBlogCASE STUDIEScareersSTRANGE Loop 2022 Contact REPL Driven Minecraft A tale of optimisation during the ClojureD Minecraft workshop [placeholder] Heading Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere. Follow @ Click to view full profile [63050405c9c4711b0149] Developer Fraser Crossman fwc@juxt.pro [] REPL Driven Minecraft [62dda3f24d] [63050405c9c47] Fraser Crossman fwc@juxt.pro Follow @ Click to view full profile August 30, 2022 Categories Clojure In June, JUXT attended ClojureD, the fantastic annual Clojure conference in Berlin. In the middle of the day, a number of workshops were run to teach you about a specific idea or tool, and get you working with it. A group of us decided to attend the 'Change the (Minecraft) World with Code' workshop run by Arne Brasseur, Ariel Alexi, and Felipe Barros. This post shows how we used what we learnt in the workshop to generate images in the game and optimised the code using Tufte. clojureD 2022 minecraft workshop The 'Change the (Minecraft) World with Code' workshop at ClojureD 2022 (Gallery) Arne showed us how to interact with the Minecraft world through the Clojure REPL! He started by showing off how we could use code to move the player around in the world and add items to their inventory. Using the expressive power of Clojure he was able to quickly start generating structures in the world with only a small set of smartly composed instructions. For his final trick, he showed us how to make chickens explode! The Witchcraft project provides a convenient API for interacting with Bukkit-based Minecraft servers and is what was used in the workshop. Having a go ourselves After showing off his REPL-driven Minecraft-ing skills, Arne left it up to us to have a go and see what we could design. Once we had everything setup and installed we were able to start up the server, connect to it with the client, and jack-in to the REPL with CIDER. The workshop repo provides four namespaces which demonstrate a variety of ways to utilise Witchcraft to orchestrate the server and Minecraft world. One of the more interesting functions provided by Witchcraft is nearest-material which finds the Minecraft material which most closely matches a given RGB colour. Witchcraft provides a file of mappings between materials and the two most prominent colours in their textures. To determine the most appropriate material to represent a given RGB value the nearest-material function calculates the colour distance to every prominent colour, and selects the material for which this value is lowest, and is therefore closest to in colour space. nearest material diag 3D plot of an RGB value and comparable materials with their prominent colours Will Caine had the brilliant idea of reading the pixels of an image and mapping them into the Minecraft world, using this function to determine the most appropriate materials to use. With a little investigation we managed to get ImageIO to read our file and before long we had RGB values for each pixel in the file! Generating images in the Minecraft world The Minecraft world is only 319 blocks high, from bedrock to the top of the map, so we had to scale the image so that it would fit. Our first implementation mapped each pixel from the image to an RGB vector lazily using for. From there we could specify a scale and sample RGB values from the large array of values. (ns gen-image (:require [lambdaisland.witchcraft :as wc] [lambdaisland.witchcraft.palette :as palette]) (:import (javax.imageio ImageIO) (java.io File) (java.awt Color))) (defn img2world [filename coords mc-width] (let [buff (. ImageIO (read (File. filename))) img-width (.getWidth buff) img-height (.getHeight buff) rgbvec (for [x (range 0 img-width)] (for [y (range 0 img-height)] (let [rgbint (.getRGB buff x y) color (Color. rgbint true)] [(.getRed color) (.getGreen color) (.getBlue color)]))) scale-factor (/ img-width mc-width) mc-height (quot img-height scale-factor)] (for [x (range 0 mc-width) y (range 0 mc-height)] (wc/set-block (-> coords (update :x + x) (update :y + y) (assoc :material (palette/nearest-material (nth (nth rgbvec (* x scale-factor)) (* y scale-factor))))))))) After hacking our solution together we finally managed to generate an image in the world. But it was upside down! juxt logo minecraft upside down The JUXT Logo, but the wrong way up :( --- (nth (nth rgbvec (* x scale-factor)) (* y scale-factor))) --- +++ (nth (nth rgbvec (* x scale-factor)) (* (- mc-height 1 y) scale-factor) +++ The origin coordinate of an image read into an ImageIO buffer is located in the top left corner of the image, not the bottom left as we had expected. This meant that as we iterated through the y-axis of the image we were descending towards the bottom. By inversing the y coordinates we were able to correctly flip the image. juxt logo minecraft The JUXT Logo the correct way up :) Optimising for speed Although our solution worked it was painfully slow to generate the resulting image. Our first thought was that all those repeated calls to wc/set-block might be slowing us down, so we refactored the code to make use of wc/set-blocks to set all the blocks at once. Using wc/ set-blocks also has the added benefit that it is much easier to undo generated images using wc/undo! as it will remove the whole image rather than just one generated block at a time. (defn img2world [filename coords mc-width] (let [buff (. ImageIO (read (File. filename))) img-width (.getWidth buff) img-height (.getHeight buff) scale-factor (/ img-width mc-width) mc-height (quot img-height scale-factor)] (wc/set-blocks (for [x (range 0 mc-width) y (range 0 mc-height)] (let [rgbint (.getRGB buff (* x scale-factor) (* y scale-factor)) color (Color. rgbint true) rgb [(.getRed color) (.getGreen color) (.getBlue color)]] [x (- mc-height 1 y) 0 (palette/nearest-material rgb)])) {:anchor coords}))) But still our solution was slow. Had we hit a hard limit? To find out what was really going on we needed to profile the code. Tufte is a simple profiler for both Clojure and ClojureScript, so we added the dependency to the server deps.edn and wrote some profiling code. To make use of Tufte you must identify the forms you would like to profile by wrapping them with p. Then call the function inside profile and observe the results. (ns gen-image (:require ... [taoensso.tufte :as tufte :refer (defnp p profile)]) ...) (defn img2world [filename coords mc-width] (let [buff (p :new-buff (. ImageIO (read (File. filename)))) img-width (.getWidth buff) img-height (.getHeight buff) scale-factor (/ img-width mc-width) mc-height (quot img-height scale-factor)] (p :set-blocks (wc/set-blocks (for [x (range 0 mc-width) y (range 0 mc-height)] (let [rgbint (p :get-rgb (.getRGB buff (* x scale-factor) (* y scale-factor))) color (p :new-color (Color. rgbint true)) rgb (p :rgb-vec [(.getRed color) (.getGreen color) (.getBlue color)])] [x (- mc-height 1 y) 0 (p :near-mat (palette/nearest-material rgb))])) {:anchor coords})))) (tufte/add-basic-println-handler! {:format-pstats-opts {:columns [:n-calls :min :max :mean :clock :total]}}) (profile {} (p :img2world (img2world "juxt-logo.png" {:x 0 :y 150 :z 0} 200))) pId nCalls Min Max Mean Clock Total :img2world 1 34.15s 34.15s 34.15s 34.15s 100% :set-blocks 1 34.14s 34.14s 34.14s 34.14s 100% :near-mat 15,400 1.47ms 28.95ms 2.18ms 33.62s 98% :get-rgb 15,400 963.00ns 6.83ms 7.39ms 113.86ms 0% :rgb-vec 15,400 124.00ns 42.28ms 1.14ms 17.49ms 0% :new-buff 1 7.26ms 7.26ms 7.26ms 7.26ms 0% :new-color 15,400 19.00ns 74.82ms 361.67ns 5.57ms 0% Accounted 1.70m 299% Clock 34.15s 100% The results show that 98% of the time spent in the function is spent in nearest-material. As we know that there is a small and constrained range of possible values for the inputs and outputs of this function, memoize can be used to effectively cache the results, mitigating the need to perform the same calculations repeatedly. This optimisation is particularly performant in this case as the image contains only a small range of different colours. --- [x (- mc-height 1 y) 0 (p (palette/nearest-material rgb))] --- +++ (def memo-nearest-material (memoize palette/nearest-material)) ... [x (- mc-height 1 y) 0 (p (memo-nearest-material rgb))] +++ pId nCalls Min Max Mean Clock Total :img2world 1 322.64ms 322.64ms 322.64ms 322.64ms 100% :set-blocks 1 317.40ms 317.40ms 317.40ms 317.40ms 98% :near-mat 15,400 395.00ns 2.55ms 10.79ms 166.12ms 51% :get-rgb 15,400 518.00ns 27.75ms 736.34ns 11.34ms 4% :new-buff 1 5.10ms 5.10ms 5.10ms 5.10ms 2% :rgb-vec 15,400 72.00ns 11.60ms 119.87ns 1.85ms 1% :new-color 15,400 20.00ns 17.50ms 43.02ns 662.46ms 0% Accounted 825.10ms 256% Clock 322.76ms 100% We can now generate the image in under a third of a second, down from 34 seconds, which is a 100x improvement. Of course, on subsequent calls the image is generated even faster as the colour-to-material mappings are already cached. wc/set-blocks is now the bottleneck, so we will leave it there. Have a go yourself If you want to have a go yourself you can easily work through it on your own by reading through the detailed workshop instructions available here. There are also some YouTube videos to help you get inspired. Thank you to Arne, Ariel, and Felipe for the brilliant workshop, to the organisers of ClojureD for running such a great conference, and to JUXT for taking us to the event. Read more Clojure Clojure in Israel: AppsFlyer Clojure in mobile attribution [5eebc2e210] Andrew Jackson July 1, 2019 Clojure-in Clojure in Seattle: World Singles Networks Clojure in the dating scene [5eebc2e210] Andrew Jackson October 10, 2018 Clojure-in Clojure in London: Gower Street Clojure in the movie business [600b178ccd] Jon Pither December 15, 2017 Analysis The Big Acquisition What Nubank's Cognitect Acquisition Means for Clojure [62f26e5104] Jamie Walkerdine August 9, 2022 Clojure AWS Lambda, now with first class parentheses Deploying Clojure on AWS Lambda with no compromises [62dfc1e382] Ray McDermott July 14, 2022 Xtdb Testing Against XTDB Faster, Simpler and more reliable [62265d8648] Peter Wilkins May 4, 2022 [5ef0bec3d4] Stay in Touch We'll let you know when new blogs are updated along with the latest in JUXT news. [ ][Sign up to the JUXT Newsletter] Thanks for signing up! We have sent you a confirmation email Oops! Something went wrong while submitting the form. Juxt logo Copyright (c) JUXT LTD. 2012-2022 Head Office Norfolk House, Silbury Blvd. Milton Keynes, MK9 2AH United Kingdom Company registration: 08457399 Sitemap HomeCase studiesAboutCareersPhotosBlogsContactClojure inRadar Follow us GitHubLinkedInTwitter Members Login XTDB [62daa8d4]GitHubZulip