├── .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 |
--------------------------------------------------------------------------------