这是我的第一个Clojure程序。如果您想在本地运行它,请查看https://github.com/achikin/game2048-clj
core.clj
(ns game2048.core
(:require [nightlight.core :refer [start]])
(:require [game2048.ui :as ui])
(:require [lanterna.screen :as s])
(:require [game2048.game :as g])
(:gen-class))
(def x 1)
(def y 1)
(defn game-loop
[scr board]
(recur
scr
(ui/draw-board
scr x y
(g/game-step (s/get-key-blocking scr) board))))
(defn -main []
(let [scr (s/get-screen) board (g/new-board)]
(s/in-screen scr
(do
(ui/draw-board scr x y board)
(ui/draw-agenda scr x (+ y (:height g/board-size) 1) g/agenda)
(game-loop scr board)))))
game.clj
(ns game2048.game
(:require [game2048.board :as b]))
(def max-score 2048)
(def board-size {:width 4 :height 4})
(def agenda
'("←↑→↓ - make move"
"r - reset"
"q - quit"))
(defn new-board []
(b/add-random-tiles (b/empty-board board-size)))
(defn process-key
"Either exit or transform board according to a key passed"
[key board]
(case key
(:up :down :left :right) (b/make-move key board)
\q (System/exit 0)
\r (b/empty-board board-size)))
(defn check-board
"Check for logical conditions and transform board accordingly"
[board-before-keypress board]
(let [board-after-rand (b/add-random-tiles board)]
(cond
(= board-before-keypress board) board
(b/full? board-after-rand) (new-board)
(b/contains-max? max-score board) (new-board)
:else board-after-rand)))
(defn game-step
[key board]
(check-board board
(process-key key board)))
ui.clj
(ns game2048.ui
(:require [game2048.board :as b])
(:require [lanterna.screen :as s]))
(def maxlen 5)
(defn count-digits [n]
(if (zero? n) 1
(-> n Math/log10 Math/floor long inc)))
(defn repeat-str
[n st]
(apply str (repeat n st)))
(defn pad-number
[length number]
(let [n (count-digits number)
pads (/ (- length n) 2)]
(apply str (repeat-str pads " ") (str number) (repeat-str pads " "))))
(defn max-number
[board]
(apply max board))
(defn max-length
[board]
(+ (count-digits (max-number board)) 2))
(defn draw-row
([screen x y row]
(if-not (empty? row)
(do
(s/put-string screen x y (pad-number maxlen (first row)))
(recur screen (+ x maxlen) y (rest row))))))
(defn draw-rows
"Draw each row and update screen"
[screen x y rows]
(if-not (empty? rows)
(do
(draw-row screen x y (first rows))
(recur screen x (inc y) (rest rows)))
(s/redraw screen)))
(defn draw-board
"Break board into horizontal rows and draw them into lanterna/screen
returns initial board for further processing"
[screen x y board]
(do
(draw-rows screen x y (b/part-board :horizontal board))
board))
(defn draw-agenda
[scr x y [first & rest]]
(if first
(do
(s/put-string scr x y first)
(recur scr x (inc y) rest))
(s/redraw scr)))
board.clj
(ns game2048.board
(:require [game2048.row :as row]))
(defn get-n-zeroes
[n]
(repeat n 0))
(defn empty-board
"Board is represented as width, height and 1 dimentional list of fields
Zero represents empty field"
[size]
(merge size {:data (get-n-zeroes (* (:width size) (:height size)))}))
(defn part-board
"Partition board list into horizontal or vertical slices"
[direction board]
(case direction
:horizontal (partition (:width board) (:data board))
:vertical (partition (:height board)
(apply interleave
(partition (:width board) (:data board))))))
(defn gather
"Gather board from horizontal or vertical slices
back into map"
[direction board]
(case direction
:horizontal {:width (-> board first count)
:height (count board)
:data (apply concat board)}
:vertical {:width (count board)
:height (-> board first count)
:data (apply interleave board)}))
(defn find-indexes
"Find all indexes of value in collection"
[val coll]
(reduce-kv
(fn [a k v] (if (= v val) (conj a k) a))
[]
coll))
(defn choose-index
"Choose random value from collection"
[indexes]
(rand-nth indexes))
(defn choose-value
"2 chosen with 2/3 probability
4 chosen with 1/3 probability"
[]
(rand-nth '(2 2 4)))
(defn rand-replace
"Replace one value in collection with another one chosen with index-fn"
[index-fn oldval value-fn coll]
(let [array (into [] coll)
indexes (find-indexes oldval array)]
(if (empty? indexes)
coll
(seq (assoc array
(index-fn indexes) (value-fn))))))
(defn add-random-tile
"Replace random zero with 2 or 4 in seq"
[board]
(rand-replace choose-index 0 choose-value board))
(defn add-random-tiles
"Replace random zero with 2 or 4 in board
in case if you want to add more than one tile"
[board]
(assoc board :data (add-random-tile (:data board))))
(defn which-partition
"Determine if move is horizontal or vertical"
[direction]
(if (contains? #{:left :right} direction)
:horizontal
:vertical))
"Up movement is eqivalent to left movement
and down movement equivalent to right movement"
(def dir-map
{:up :left
:down :right
:left :left
:right :right})
(defn make-move
"Break board into either horizontal or vertical slices
perform move on each slice, and gather result back into new board"
[direction board]
(let [part (which-partition direction)]
(gather part
(map #(row/move (direction dir-map) %) (part-board part board)))))
(defn full?
"True if there are no empty(0) fields left"
[board]
(not-any? #{0} (:data board)))
(defn contains-max?
"True if one of the sells reached maximum value"
[max-score board]
(not (empty? (filter #(= max-score %) (:data board)))))
row.clj
(ns game2048.row)
(defmulti padd (fn [direction len coll] direction))
"Pad collections with zeroes either on the left or on the right"
(defmethod padd :left
[direction len coll]
(concat (repeat (- len (count coll)) 0) coll))
(defmethod padd :right
[direction len coll]
(concat coll (repeat (- len (count coll)) 0)))
(defmulti merger(fn [dir & args] dir))
"Check if there are equal adjustent fields and merge them
e.g. (merger :left '(1 1 2 2)) -> (2 4)"
(defmethod merger :left
([dir [first second & rest] newrow]
(if first
(if (= first second)
(recur dir rest (cons (+ first second) newrow))
(recur dir (cons second rest) (cons first newrow)))
(reverse newrow)))
([dir row]
(merger dir row '())))
(defmethod merger :right
[dir row]
(reverse (merger :left (reverse row))))
(defn remove-zeroes
"Return collection dropping all zeroes"
[coll]
(filter (fn [x] (not (zero? x)))
coll))
(defn opposite-dir
[dir]
(case dir
:left :right
:right :left))
(defn move
"Remove zeroes, then merge values, then pad result with zeroes
e.g. (move :left '(1 1 0 2 2) -> (1 1 2 2) -> (2 4 0 0 0)"
[dir row]
(let [row-size (count row)]
(padd (opposite-dir dir) row-size (merger dir (remove-zeroes row)))))
board_test.clj
(ns game2048.board-test
(:use clojure.test)
(:require [game2048.board :as b]))
(def empty-board-3-3
{:width 3
:height 3
:data '(0 0 0 0 0 0 0 0 0)})
(def empty-board-2-4
{:width 2
:height 4
:data '(0 0 0 0 0 0 0 0)})
(deftest empty-board
(is (= (b/empty-board {:width 3 :height 3}) empty-board-3-3))
(is (= (b/empty-board {:width 2 :height 4}) empty-board-2-4)))
(def part-board-2-2
{:width 2
:height 2
:data '(1 1 2 2)})
(def part-board-2-2-left
{:width 2
:height 2
:data '(2 0 4 0)})
(def part-board-2-2-horizontal '((1 1)(2 2)))
(def part-board-2-2-vertical '((1 2)(1 2)))
(deftest part-board
(is (= (b/part-board :horizontal part-board-2-2) part-board-2-2-horizontal))
(is (= (b/part-board :vertical part-board-2-2) part-board-2-2-vertical)))
(deftest gather
(is (= (b/gather :horizontal '((1 1) (2 2))) part-board-2-2))
(is (= (b/gather :vertical '((1 2) (1 2))) part-board-2-2)))
(defn index-fn-1
[coll]
1)
(defn index-fn-3
[coll]
3)
(defn value-fn-2
[]
2)
(defn value-fn-4
[]
4)
(deftest rand-replace
(is (= (b/rand-replace index-fn-1 0 value-fn-2 '(0 0 0)) '(0 2 0)))
(is (= (b/rand-replace index-fn-3 0 value-fn-4 '(0 0 0 0 0)) '(0 0 0 4 0))))
(def board-move
{:width 2
:height 2
:data '(2 4 2 4)})
(def board-move-up
{:width 2
:height 2
:data '(4 8 0 0)})
(def board-move-down
{:width 2
:height 2
:data '(0 0 4 8)})
(deftest make-move
(is (= (b/make-move :left part-board-2-2) part-board-2-2-left))
(is (= (b/make-move :up board-move) board-move-up))
(is (= (b/make-move :down board-move) board-move-down))
(is (= (b/make-move :right board-move) board-move)))
(deftest full
(is (b/full? {:width 2 :height 2 :data '(1 2 3 4)}))
(is (not (b/full? {:width 2 :height 2 :data '(1 2 3 0)}))))
(deftest contains-max
(is (b/contains-max? 2048 {:width 2 :height 2 :data '(1 2 2048 4)})))
流程图
发布于 2016-12-09 23:22:47
core.clj
(def X 1) (def Y 1)
x-origin
和y-origin
这样的东西会更好。bindings
在不同的位置呈现游戏(例如,如果您想为面对面的竞争呈现两个游戏)。*origin*
变量。将这些要点结合在一起,您最终会得到如下内容:
(def ^:dynamic *origin*
"Defines the origin at which to render the game"
[1 1])
game.clj
( '("←↑→↓-作出移动“"r -重置”"q -退出“))
我喜欢你在这里采用以数据为中心的方法。这是个很好的模式。
(根据已传递的密钥“退出或转换板”键板)
agenda
)来将其解耦。case
中没有子句,因此如果按下任何其他键,此代码将引发异常。System/exit
来结束游戏限制了你以后可以做的事情。例如,你不能回到游戏菜单。除非绝对必要,否则不要使用System/exit
。在这种情况下,避免这种情况的一种方法是返回nil
。如果遇到nil
,则让主循环终止。使用以数据为中心的方法,我会这样做:
(def ^:dynamic *actions*
"Maps keys to action fns. Each action is a function that takes a board state and returns a new board state."
{:up b/move-up
:down b/move-down
:left b/move-left
:right b/move-right
\q (constantly nil)
\r (fn [board]
(b/empty-board board-size))})
然后可以将process-key
定义为:
(defn process-key
"Either exit or transform board according to a key passed"
([key board]
(let [action (get *actions* key identity)]
(action board))))
当无法映射密钥时,使用identity
作为缺省值来提供有效的无操作。
board.clj
所有操纵董事会状态的功能都直接涉及到董事会的内部代表--没有抽象。您应该考虑定义一个协议来定义处理板的接口。有很多种方法。这里有一个选择:
(defprotocol IBoard
(rows [board] [board data])
(columns [board] [board data])
然后使用deftype
提供一个实现。
现在,我们可以定义如下的move-
函数:
(defn move-up [board]
(columns board (map compress (columns board))))
(defn move-down [board]
(columns board (map reverse (map compress (map reverse (columns board))))))
尽管使用->>
更为惯用:
(defn move-left [board]
(->> (rows board) ;; Obtain row-centric view of the board
(map compress) ;; "Compress" each row
(rows board))) ;; Construct new board from compressed rows
(defn move-right [board]
(->> (rows board) ;;
(map reverse) ;; Reverse rows, so compression happens in correct order
(map compress) ;; "Compress" each row
(map reverse) ;; Reverse rows back to original order
(rows board))) ;; Construct new board from compressed rows
如果需要,可以重构这些元素以利用共享结构。
compress
接受序列并用其和的单个元素替换连续的元素。要使上述函数工作,compress
的结果必须与其输入具有相同的长度(即与0
S填充)。否则,你将不得不处理其他地方的填充。
我相信这个设计会让你摆脱row.clj
。
ui.clj
(def maxlen 5)
我认为你应该避免这里的严格限制。至少,要使maxlen
动态化。但是更好的是,通过max-length
动态地计算它(您目前不使用它,BTW)。如果这样做,您可能需要一个minlen
,以避免单元格大小更改太频繁。
(N数位数)
这太复杂了。只需将数字转换为字符串并计数字符串中的字符数:
(defn count-digits [n]
(count (str n)))
或者,使用String
的S length
属性:
(defn count-digits [n]
(. (str n) length))
(draw (屏幕x y行))
如果有合适的替代方案,请避免使用recur
。在本例中,我将使用doseq
:
(defn draw-row
([screen x y row]
(doseq [[i n] (zip (range) row)]
(let [x' (+ x (* i maxlen))
n' (pad-number maxlen n)]
s/put-string screen x' y n'))))
draw-rows
也是如此。
另外,您正在调用s/redraw
in draw-rows
。我认为从draw-board
中调用它会更有意义。但实际上,重新绘制屏幕可以说是一个独立的问题,而不是渲染董事会。因此,最好在更高的级别进行重绘,例如主循环。
https://codereview.stackexchange.com/questions/148151
复制相似问题