首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >2048游戏在Clojure中的实现

2048游戏在Clojure中的实现
EN

Code Review用户
提问于 2016-11-26 09:49:40
回答 1查看 356关注 0票数 6

这是我的第一个Clojure程序。如果您想在本地运行它,请查看https://github.com/achikin/game2048-clj

core.clj

代码语言:javascript
运行
复制
(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

代码语言:javascript
运行
复制
(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

代码语言:javascript
运行
复制
(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

代码语言:javascript
运行
复制
(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

代码语言:javascript
运行
复制
(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

代码语言:javascript
运行
复制
(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)})))

流程图

EN

回答 1

Code Review用户

回答已采纳

发布于 2016-12-09 23:22:47

core.clj

(def X 1) (def Y 1)

  1. 使用更多的描述性名称。这些似乎被用作渲染游戏的起源,所以也许像x-originy-origin这样的东西会更好。
  2. 添加文档字符串以记录每个var /函数的意义和用途
  3. 这可能是值得的-同时制造这些动态vars。然后,您可以使用bindings在不同的位置呈现游戏(例如,如果您想为面对面的竞争呈现两个游戏)。
  4. 您可以将这两者合并成一个*origin*变量。

将这些要点结合在一起,您最终会得到如下内容:

代码语言:javascript
运行
复制
(def ^:dynamic *origin*
  "Defines the origin at which to render the game"
  [1 1])

game.clj

( '("←↑→↓-作出移动“"r -重置”"q -退出“))

我喜欢你在这里采用以数据为中心的方法。这是个很好的模式。

(根据已传递的密钥“退出或转换板”键板)

  1. 你有钥匙硬编码他们的行动。您可以通过使用以数据为中心的方法(类似于agenda)来将其解耦。
  2. 您的case中没有子句,因此如果按下任何其他键,此代码将引发异常。
  3. 通过调用System/exit来结束游戏限制了你以后可以做的事情。例如,你不能回到游戏菜单。除非绝对必要,否则不要使用System/exit。在这种情况下,避免这种情况的一种方法是返回nil。如果遇到nil,则让主循环终止。

使用以数据为中心的方法,我会这样做:

代码语言:javascript
运行
复制
(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定义为:

代码语言:javascript
运行
复制
(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

所有操纵董事会状态的功能都直接涉及到董事会的内部代表--没有抽象。您应该考虑定义一个协议来定义处理板的接口。有很多种方法。这里有一个选择:

代码语言:javascript
运行
复制
(defprotocol IBoard
  (rows [board] [board data])
  (columns [board] [board data])

然后使用deftype提供一个实现。

现在,我们可以定义如下的move-函数:

代码语言:javascript
运行
复制
(defn move-up [board]
  (columns board (map compress (columns board))))

(defn move-down [board]
  (columns board (map reverse (map compress (map reverse (columns board))))))

尽管使用->>更为惯用:

代码语言:javascript
运行
复制
(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的结果必须与其输入具有相同的长度(即与0S填充)。否则,你将不得不处理其他地方的填充。

我相信这个设计会让你摆脱row.clj

ui.clj

(def maxlen 5)

我认为你应该避免这里的严格限制。至少,要使maxlen动态化。但是更好的是,通过max-length动态地计算它(您目前不使用它,BTW)。如果这样做,您可能需要一个minlen,以避免单元格大小更改太频繁。

(N数位数)

这太复杂了。只需将数字转换为字符串并计数字符串中的字符数:

代码语言:javascript
运行
复制
(defn count-digits [n]
  (count (str n)))

或者,使用String的S length属性:

代码语言:javascript
运行
复制
(defn count-digits [n]
  (. (str n) length))

(draw (屏幕x y行))

如果有合适的替代方案,请避免使用recur。在本例中,我将使用doseq

代码语言:javascript
运行
复制
(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中调用它会更有意义。但实际上,重新绘制屏幕可以说是一个独立的问题,而不是渲染董事会。因此,最好在更高的级别进行重绘,例如主循环。

票数 2
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/148151

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档