Loading...
I hope you like it. I find it fun. It only took two hours to build. So what was the process to build it?
Figwhat? Rewho? why?
This post is intended to motivate you by showing something how fast you can build things. The full code listing is here: https://github.com/timothypratley/tetrisI have used many frameworks/languages/editors/styles, and nothing comes close to Figwheel/Reagent. A lot of that is because it almost feels like they aren't even there. I'm editing Clojure code in one window, and seeing my browser update in another window. I'm writing hiccup style HTML as a function of data, and the UI is in sync with my data model. I'm writing data transition rules, and my game is moving. I'm not going to go into details about the technology, as others have explained them eloquently here:
- Reagent (Matt Ho)
- Clojure (Rich Hickey)
- Figwheel (Bruce Hauman)
- ClojureScript (David Nolen)
Why not give it a shot? It will not take much of your time to walk through some of the great tutorials you can find at the figwheel and reagent pages.
Tetris is a block world; use a matrix to represent the screen and rules.
The world consists of a piece matrix, x, y, block-pile matrix, and score.
The world transitions in steps which need validation.
lein figwheel
open http://localhost:3449
Edit src/tetris/core.cljs, see new text on the page immediately.
I'll represent pieces as a matrix, here are the bar and square:
Rendering a piece is a matter of drawing a block for each 1 in the piece matrix. This is hiccup syntax which will be converted into regular HTML tags. If you inspect the elements of the game, you will see the SVG tags updating as the data changes.
Creating a world to render:
Great we can see a Tetris piece on the screen!
Wohoo - the piece is falling, this looks like Tetris.
Now the piece stops at the bottom of the screen.
Pieces are stacking up when they hit the ground now.
Quite Easy Done; the valid-world? check keeps the piece in the world, so the user goes off the screen.
This Tetris thing was kind of a kata. I have in the past turned my nose up at katas; something about them strikes me as frivolous. Katas do provide a vehicle to think about your process. With an open mind you can examine your problem solving, creativity, tools, and workflow. However, to me the most valuable thing you can do is formulate a problem well. What problem should I formulate and solve? That is what I want to get better at doing, not the kata itself. A kata already has a neat problem formulation. Perhaps I should practice by writing katas instead of implementing them.
The workflow I have described hereto benefits greatly from save on focus lost. It is a natural punctuation point between coding and viewing the results.
Figwheel can be replaced by boot-reload. I think eventually boot will win because it has a build pipeline that allows you to compose things like cross compilation with linting and test execution. However Figwheel is still better in the short term because of the HUD. It shows errors and warnings where you want to see them, and filters out ridiculous stack traces. Reagent was preceded by Om which remains an excellent choice. For me Reagent wins on brevity.
Here is the process I went through to build this Tetris clone:
10:45am - I want to build Tetris
Understand the problem - write out a list of features
- Start empty world, with a random piece
- Piece drops every t
- user can spin and slide the piece with keyboard input
- when piece lands (collides), full rows are removed, new piece comes down
- stop when no space is left
11:00am - Thinking
Making connections, ideas, a plan
Use inline HTML SVG tags; they are visible in inspect elements and require no dependencies.Tetris is a block world; use a matrix to represent the screen and rules.
The world consists of a piece matrix, x, y, block-pile matrix, and score.
The world transitions in steps which need validation.
11:15am - Represent the base case world
Execute the plan
Setting up a new project:
lein new figwheel tetris -- --reagent
cd tetrislein figwheel
open http://localhost:3449
Edit src/tetris/core.cljs, see new text on the page immediately.
I'll represent pieces as a matrix, here are the bar and square:
(def pieces [[[1 1 1 1]] [[1 1] [1 1]]]) (defn block [x y color] [:rect {:x x :y y :width 1 :height 1 :stroke "black" :stroke-width 0.01 :rx 0.1 :fill (world/colors color)}]) (defn board-view [{:keys [piece color x y]}] (let [piece-width (count piece) piece-height (count (first piece))] (into [:svg {:style {:border "1px solid black" :width 200 :height 400} :view-box (string/join " " [0 0 10 20])}] (for [i (range piece-width) j (range piece-height) :when (pos? (get-in piece [i j]))] [block (+ x i) (+ y j) color]))))
Rendering a piece is a matter of drawing a block for each 1 in the piece matrix. This is hiccup syntax which will be converted into regular HTML tags. If you inspect the elements of the game, you will see the SVG tags updating as the data changes.
Creating a world to render:
(defn with-new-peice [world] (let [peice (rand-peice)] (assoc world :x (- 5 (quot (count peice) 2)) :y 0 :peice peice :color (rand-int (count colors))))) (defn new-world [] (with-new-peice {:score 0 :block-pile (make-block-pile 10 20)}))
Great we can see a Tetris piece on the screen!
11:30am - Introduce time and gravity
I need a recurring update of the world which changes the piece y coord:
(defn move-down [world] (update-in world [:y] inc)) (defn gravity [world] (let [new-world (move-down world)] (if (valid-world? new-world) new-world (landed world)))) (defn tick! [] (when-not (:done @app-state) (swap! app-state gravity))) (js/setInterval world/tick! 200))
Wohoo - the piece is falling, this looks like Tetris.
(defn valid-world? [{:keys [x y piece block-pile done]}] (every? #{-1} (for [i (range (count piece)) j (range (count (first piece))) :when (pos? (get-in piece [i j])) :let [matrix-x (+ x i) matrix-y (+ y j)]] (get-in block-pile [matrix-x matrix-y])))) (defn maybe-step [world f] (let [new-world (f world)] (if (world/valid-world? new-world) (reset! world/app-state new-world) world)))
Now the piece stops at the bottom of the screen.
11:45am - Landed blocks absorbed into block pile
Upon landing:- Copy the piece into the block pile matrix.
- Remove completed rows.
- Update the score.
- Reset to a new random piece.
- If the new piece does not fit, game over.
(defn complete? [row] (not-any? #{-1} row)) (defn with-completed-rows [{:as world :keys [block-pile]}] (let [remaining-rows (remove complete? (transpose block-pile)) cc (- 20 (count remaining-rows)) new-rows (repeat cc (vec (repeat 10 -1)))] (-> world (update-in [:score] inc) (update-in [:score] + (* 10 cc cc)) (assoc :block-pile (transpose (concat new-rows remaining-rows)))))) (defn collect-piece [block-pile [x y color]] (assoc-in block-pile [x y] color)) (defn push-piece [{:as world :keys [piece color x y block-pile]}] (let [piece-width (count piece) piece-height (count (first piece))] (assoc world :block-pile (reduce collect-piece block-pile (for [i (range piece-width) j (range piece-height) :when (pos? (get-in piece [i j]))] [(+ x i) (+ y j) color]))))) (defn maybe-done [world] (if (valid-world? world) world (assoc world :done true))) (defn landed [world] (-> world push-piece with-completed-rows with-new-piece maybe-done))
12:00 - Debugging
Throughout this process I have been debugging as I go. Mainly that consists of printing values that then appear in the console and making adjustments from there. Here I hit a bump where I've made two large changes at once and need to do a bit of backtracking to find out the cause and fix it. There is a REPL available for me to do more fine grained testing, but I rarely bother with it, as I am in the flow of changing code and looking for the output in the browser.Pieces are stacking up when they hit the ground now.
12:15 - Keyboard handling
Hook up some key event handling logic:(def action {"LEFT" world/move-left "RIGHT" world/move-right "UP" world/rotate "SPACE" world/rotate "DOWN" world/drop-to-ground}) (defn handle-keydown [e] (when-not (:done @world/app-state) (when-let [f (action (codename (.-keyCode e)))] (.preventDefault e) (swap! world/app-state maybe-step f))))
Quite Easy Done; the valid-world? check keeps the piece in the world, so the user goes off the screen.
12:30 - Break
I am proud of my creation. It is fun to play. There are rough edges and missing features. Motivation dwindles so I take a break.4:00pm - Clean up for publication
Improving the colors, shapes, adding a soundtrack, split the code base into namespaces. I am not concentrating intently, just taking 15min here and there between eating, watching a movie and relaxing. I spend a lot of time playing the game because it is fun. I don't consider this core development time.Next day - Blogging about it
Review, discuss, make new connections
Building Tetris turned out to be simple and concise. I didn't need any special purpose libraries. ClojureScript/Reagent/Figwheel are general purpose language/tooling level concerns that keep out of my way. So long as I know some Clojure and HTML, I can build away to my hearts content. This is dramatically different to using say AngularJS where I have to learn a whole new paradigm, api, and rely on external packages for non-trivial components.The workflow I have described hereto benefits greatly from save on focus lost. It is a natural punctuation point between coding and viewing the results.
Figwheel can be replaced by boot-reload. I think eventually boot will win because it has a build pipeline that allows you to compose things like cross compilation with linting and test execution. However Figwheel is still better in the short term because of the HUD. It shows errors and warnings where you want to see them, and filters out ridiculous stack traces. Reagent was preceded by Om which remains an excellent choice. For me Reagent wins on brevity.