├── .gitignore ├── README.md ├── project.clj ├── resources └── public │ ├── css │ └── style.css │ ├── favicon.ico │ └── index.html └── src └── tetris ├── core.cljs ├── view.cljs └── world.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | /resources/public/js/compiled/** 2 | figwheel_server.log 3 | pom.xml 4 | *jar 5 | /lib/ 6 | /classes/ 7 | /out/ 8 | /target/ 9 | .lein-deps-sum 10 | .lein-repl-history 11 | .lein-plugins/ 12 | .repl 13 | .nrepl-port 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tetris 2 | 3 | Start empty world, with a random peice. 4 | Peice drops every t. 5 | User can spin and straffe the peice. 6 | When peice lands (collides), full rows are removed. 7 | New peice comes down when peice lands. 8 | Stop when no space. 9 | 10 | * svg 11 | * keyboard input rotate l/r space 12 | * matrix for screen/rules 13 | * matrix (represeting the world) 14 | * -> matrix (next-step world) 15 | * our peice 16 | * x, y, rotation, matrix 17 | * score 18 | 19 | peices: 20 | 21 | 11 22 | 11 23 | 24 | 11 25 | 11 26 | 27 | block-pile: 28 | 29 | 000000 30 | 000000 31 | 000000 32 | 000011 33 | 111111 34 | 100110 35 | 36 | -> 37 | 38 | 000000 39 | 000011 40 | 100110 41 | 42 | 43 | ## Development 44 | 45 | lein figwheel 46 | 47 | Open your browser at [localhost:3449](http://localhost:3449/). 48 | 49 | To create a production build run: 50 | 51 | lein cljsbuild once min 52 | 53 | And open `resources/public/index.html`. 54 | 55 | 56 | ## License 57 | 58 | Copyright © 2014 Timothy Pratley 59 | 60 | Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version. 61 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject tetris "0.1.0-SNAPSHOT" 2 | :description "FIXME: write this!" 3 | :url "http://example.com/FIXME" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :dependencies [[org.clojure/clojure "1.7.0"] 8 | [org.clojure/clojurescript "0.0-3297"] 9 | [org.clojure/core.async "0.1.346.0-17112a-alpha"] 10 | [reagent "0.5.0"]] 11 | 12 | :plugins [[lein-cljsbuild "1.0.5"] 13 | [lein-figwheel "0.3.5"]] 14 | 15 | :source-paths ["src"] 16 | 17 | :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] 18 | 19 | :cljsbuild { 20 | :builds [{:id "dev" 21 | :source-paths ["src"] 22 | 23 | :figwheel { :on-jsload "tetris.core/on-js-reload" } 24 | 25 | :compiler {:main tetris.core 26 | :asset-path "js/compiled/out" 27 | :output-to "resources/public/js/compiled/tetris.js" 28 | :output-dir "resources/public/js/compiled/out" 29 | :source-map-timestamp true }} 30 | {:id "min" 31 | :source-paths ["src"] 32 | :compiler {:output-to "resources/public/js/compiled/tetris.js" 33 | :main tetris.core 34 | :optimizations :advanced 35 | :pretty-print false}}]} 36 | 37 | :figwheel { 38 | ;; :http-server-root "public" ;; default and assumes "resources" 39 | ;; :server-port 3449 ;; default 40 | ;; :server-ip "127.0.0.1" 41 | 42 | :css-dirs ["resources/public/css"] ;; watch and update CSS 43 | 44 | ;; Start an nREPL server into the running figwheel process 45 | ;; :nrepl-port 7888 46 | 47 | ;; Server Ring Handler (optional) 48 | ;; if you want to embed a ring handler into the figwheel http-kit 49 | ;; server, this is for simple ring servers, if this 50 | ;; doesn't work for you just run your own server :) 51 | ;; :ring-handler hello_world.server/handler 52 | 53 | ;; To be able to open files in your editor from the heads up display 54 | ;; you will need to put a script on your path. 55 | ;; that script will have to take a file path and a line number 56 | ;; ie. in ~/bin/myfile-opener 57 | ;; #! /bin/sh 58 | ;; emacsclient -n +$2 $1 59 | ;; 60 | ;; :open-file-command "myfile-opener" 61 | 62 | ;; if you want to disable the REPL 63 | ;; :repl false 64 | 65 | ;; to configure a different figwheel logfile path 66 | ;; :server-logfile "tmp/logs/figwheel-logfile.log" 67 | }) 68 | -------------------------------------------------------------------------------- /resources/public/css/style.css: -------------------------------------------------------------------------------- 1 | /* some style */ 2 | 3 | -------------------------------------------------------------------------------- /resources/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothypratley/tetris/5b91b0ef91b1a53847493d05c66d946bbf2efccb/resources/public/favicon.ico -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tetris 8 | 9 | 10 |
11 |

Loading...

12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/tetris/core.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-always tetris.core 2 | (:require [tetris.world :as world] 3 | [tetris.view :as view] 4 | [reagent.core :as reagent])) 5 | 6 | (enable-console-print!) 7 | 8 | (defn maybe-step [world f] 9 | (let [new-world (f world)] 10 | (if (world/valid-world? new-world) 11 | (reset! world/app-state new-world) 12 | world))) 13 | 14 | (def codename 15 | {37 "LEFT" 16 | 38 "UP" 17 | 39 "RIGHT" 18 | 40 "DOWN" 19 | 32 "SPACE"}) 20 | 21 | (def action 22 | {"LEFT" world/move-left 23 | "RIGHT" world/move-right 24 | "UP" world/rotate 25 | "SPACE" world/rotate 26 | "DOWN" world/drop-to-ground}) 27 | 28 | (defn handle-keydown [e] 29 | (when-not (:done @world/app-state) 30 | (when-let [f (action (codename (.-keyCode e)))] 31 | (.preventDefault e) 32 | (swap! world/app-state maybe-step f)))) 33 | 34 | (defn on-js-reload [] 35 | (println "Reloaded...") 36 | (reset! world/app-state (world/new-world)) 37 | (reagent/render-component [view/root-view] (. js/document (getElementById "app")))) 38 | 39 | (defn init [] 40 | (on-js-reload) 41 | (.addEventListener js/document "keydown" handle-keydown) 42 | (js/setInterval world/tick! 200)) 43 | 44 | (defonce start 45 | (init)) 46 | -------------------------------------------------------------------------------- /src/tetris/view.cljs: -------------------------------------------------------------------------------- 1 | (ns tetris.view 2 | (:require [tetris.world :as world] 3 | [clojure.string :as string])) 4 | 5 | (defn block [x y color] 6 | [:rect {:x x 7 | :y y 8 | :width 1 9 | :height 1 10 | :stroke "black" 11 | :stroke-width 0.01 12 | :rx 0.1 13 | :fill (world/colors color)}]) 14 | 15 | (defn board-view [{:keys [piece color x y block-pile done]}] 16 | (let [piece-width (count piece) 17 | piece-height (count (first piece)) 18 | block-width (count block-pile) 19 | block-height (count (first block-pile))] 20 | [:svg {:style {:border "1px solid black" 21 | :width 200 22 | :height 400} 23 | :view-box (string/join " " [0 0 10 20])} 24 | (when-not done 25 | (into [:g {:name "current piece"}] 26 | (for [i (range piece-width) 27 | j (range piece-height) 28 | :when (pos? (get-in piece [i j]))] 29 | [block (+ x i) (+ y j) color]))) 30 | (into [:g {:name "block pile"}] 31 | (for [i (range block-width) 32 | j (range block-height) 33 | :let [block-color (get-in block-pile [i j])] 34 | :when (not (neg? block-color))] 35 | [block i j block-color]))])) 36 | 37 | (defn tetris-view [{:as world :keys [done score]}] 38 | [:div {:style {:font-family "Courier New" 39 | :text-align "center"}} 40 | [:h1 (if done "Game Over" "Tetris")] 41 | [board-view world] 42 | [:h2 [(if done :blink :span) "Score " score]] 43 | [:audio {:controls "true" 44 | :auto-play "true" 45 | :loop "true"} 46 | [:source {:src "https://archive.org/download/Tetris_570/Tetris.ogg" 47 | :type "audio/ogg"}] 48 | [:source {:src "https://archive.org/download/Tetris_570/Tetris.mp3" 49 | :type "audio/mpeg"}] 50 | "Your browser does not support the audio element."] 51 | [:br] 52 | (when done 53 | [:button {:on-click (fn restart-click [e] 54 | (reset! world/app-state (world/new-world))) 55 | :style {:width 200 56 | :padding "10px 20px 10px 20px" 57 | :font-family "Courier New" 58 | :font-size 16}} 59 | "Restart"])]) 60 | 61 | (defn root-view [] 62 | [tetris-view @world/app-state]) 63 | -------------------------------------------------------------------------------- /src/tetris/world.cljs: -------------------------------------------------------------------------------- 1 | (ns tetris.world 2 | (:require [reagent.core :as reagent])) 3 | 4 | (defonce app-state 5 | (reagent/atom {})) 6 | 7 | (defn make-block-pile [x y] 8 | (vec (repeat x (vec (repeat y -1))))) 9 | 10 | (def pieces 11 | [[[0 0 0 0] 12 | [0 0 0 0] 13 | [1 1 1 1] 14 | [0 0 0 0] 15 | [0 0 0 0]] 16 | [[1 1] 17 | [1 1]] 18 | [[1 0] 19 | [1 1] 20 | [0 1]] 21 | [[1 0] 22 | [1 0] 23 | [1 1]] 24 | [[1 1 1] 25 | [0 1 0]]]) 26 | 27 | (defn transpose [matrix] 28 | (apply mapv vector matrix)) 29 | 30 | (defn flip [matrix] 31 | (vec (reverse matrix))) 32 | 33 | (defn rand-piece [] 34 | (transpose (rand-nth pieces))) 35 | 36 | (def colors 37 | ["#181818" 38 | "#585858" 39 | "#D8D8D8" 40 | "#AB4642" 41 | "#DC9656" 42 | "#F7CA88" 43 | "#A1B56C" 44 | "#86C1B9" 45 | "#7CAFC2" 46 | "#BA8BAF" 47 | "#A16946"]) 48 | 49 | (defn with-new-piece [world] 50 | (let [piece (rand-piece)] 51 | (assoc world 52 | :x (- 5 (quot (count piece) 2)) 53 | :y 0 54 | :piece piece 55 | :color (rand-int (count colors))))) 56 | 57 | (defn new-world [] 58 | (with-new-piece 59 | {:score 0 60 | :block-pile (make-block-pile 10 20)})) 61 | 62 | (defn valid-world? [{:keys [x y piece block-pile done]}] 63 | (every? #{-1} 64 | (for [i (range (count piece)) 65 | j (range (count (first piece))) 66 | :when (pos? (get-in piece [i j])) 67 | :let [matrix-x (+ x i) 68 | matrix-y (+ y j)]] 69 | (get-in block-pile [matrix-x matrix-y])))) 70 | 71 | (defn complete? [row] 72 | (not-any? #{-1} row)) 73 | 74 | (defn with-completed-rows [{:as world :keys [block-pile]}] 75 | (let [remaining-rows (remove complete? (transpose block-pile)) 76 | cc (- 20 (count remaining-rows)) 77 | new-rows (repeat cc (vec (repeat 10 -1)))] 78 | (-> world 79 | (update-in [:score] inc) 80 | (update-in [:score] + (* 10 cc cc)) 81 | (assoc :block-pile (transpose (concat new-rows remaining-rows)))))) 82 | 83 | (defn collect-piece [block-pile [x y color]] 84 | (assoc-in block-pile [x y] color)) 85 | 86 | (defn push-piece [{:as world :keys [piece color x y block-pile]}] 87 | (let [piece-width (count piece) 88 | piece-height (count (first piece))] 89 | (assoc world :block-pile 90 | (reduce collect-piece block-pile 91 | (for [i (range piece-width) 92 | j (range piece-height) 93 | :when (pos? (get-in piece [i j]))] 94 | [(+ x i) (+ y j) color]))))) 95 | 96 | (defn maybe-done [world] 97 | (if (valid-world? world) 98 | world 99 | (assoc world :done true))) 100 | 101 | (defn landed [world] 102 | (-> world 103 | push-piece 104 | with-completed-rows 105 | with-new-piece 106 | maybe-done)) 107 | 108 | (defn move-down [world] 109 | (update-in world [:y] inc)) 110 | 111 | (defn gravity [world] 112 | (let [new-world (move-down world)] 113 | (if (valid-world? new-world) 114 | new-world 115 | (landed world)))) 116 | 117 | (defn tick! [] 118 | (when-not (:done @app-state) 119 | (swap! app-state gravity))) 120 | 121 | (defn move-left [world] 122 | (update-in world [:x] dec)) 123 | 124 | (defn move-right [world] 125 | (update-in world [:x] inc)) 126 | 127 | (defn rotate [world] 128 | (update-in world [:piece] (comp transpose flip))) 129 | 130 | (defn drop-to-ground [world] 131 | (landed (last (take-while valid-world? (iterate move-down world))))) 132 | --------------------------------------------------------------------------------