Last active
October 19, 2017 06:08
-
-
Save kohyama/5828936 to your computer and use it in GitHub Desktop.
ぷよぷよ連鎖 in Clojure
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;;; Copyright (c) 2013 Yoshinori Kohyama. Distributed under the BSD 3-Clause License. | |
(ns puyo | |
(:require [clojure.test :refer (with-test run-tests are)] | |
[clojure.set :refer (union)] | |
[clojure.string :as string])) | |
(with-test | |
(defn- fall-one [b s] | |
(->> (reverse b) | |
(apply map vector) | |
(map #(reduce | |
(fn [[h & r] x] | |
(if h | |
(if (and (= h s) (not= x s)) | |
(cons h (cons x r)) | |
(cons x (cons h r))) | |
(list x))) | |
() | |
%)) | |
(apply map vector) | |
vec)) | |
(are [b s a] (= (fall-one b s) a) | |
[[:s :s :s :A] | |
[:A :s :s :s] | |
[:A :B :s :D] | |
[:s :s :C :s]] | |
:s | |
[[:s :s :s :s] | |
[:s :s :s :A] | |
[:A :s :s :s] | |
[:A :B :C :D]])) | |
(with-test | |
(defn- connect | |
"Separate sets of sets 'ss' into two group, | |
by if it has one or more elements equals to one of 'ev' or not. | |
And take the union of the former and conj 'e' to it, | |
and conj it to the latter." | |
[ev ss e] | |
(let [hn (group-by (fn [s] (some (fn [e] (some #(= e %) s)) ev)) ss) | |
h (hn true) | |
n (set (hn nil))] | |
(conj n (conj (apply union h) e)))) | |
(are [ev ss e nss] (= (connect ev ss e) nss) | |
[:a] #{#{:a :b} #{:c} #{:d :e :f}} :g #{#{:a :b :g} #{:c} #{:d :e :f}} | |
[:c] #{#{:a :b} #{:c} #{:d :e :f}} :g #{#{:a :b} #{:c :g} #{:d :e :f}} | |
[:d] #{#{:a :b} #{:c} #{:d :e :f}} :g #{#{:a :b} #{:c} #{:d :e :f :g}} | |
[:e] #{#{:a :b} #{:c} #{:d :e :f}} :g #{#{:a :b} #{:c} #{:d :e :f :g}} | |
[:a :b] #{#{:a :b} #{:c} #{:d :e :f}} :g #{#{:a :b :g} #{:c} #{:d :e :f}} | |
[:a :c] #{#{:a :b} #{:c} #{:d :e :f}} :g #{#{:a :b :c :g} #{:d :e :f}} | |
[:a :d] #{#{:a :b} #{:c} #{:d :e :f}} :g #{#{:a :b :d :e :f :g} #{:c}} | |
[:c :d] #{#{:a :b} #{:c} #{:d :e :f}} :g #{#{:a :b} #{:c :d :e :f :g}} | |
[:d :e] #{#{:a :b} #{:c} #{:d :e :f}} :g #{#{:a :b} #{:c} #{:d :e :f :g}} | |
[] #{#{:a :b} #{:c} #{:d :e :f}} :g #{#{:a :b} #{:c} #{:d :e :f} #{:g}} | |
[] #{} :g #{#{:g}})) | |
(with-test | |
(defn- grouped-indices [b] | |
(let [h (count b) | |
w (count (first b))] | |
(reduce | |
(fn [a [y x :as yx]] | |
(let [c (get-in b yx) | |
uyx [(dec y) x] | |
lyx [y (dec x)]] | |
(connect (filter #(= (get-in b %) c) [uyx lyx]) a yx))) | |
#{} | |
(for [y (range h) x (range w)] [y x])))) | |
(are [b g] (= (grouped-indices b) g) | |
[[:A :s :A :A] [:A :A :C :C] [:C :B :B :B]] | |
#{#{[0 0] [1 0] [1 1]} ; :A | |
#{[0 1]} ; :s | |
#{[0 2] [0 3]} ; :A | |
#{[1 2] [1 3]} ; :C | |
#{[2 0]} ; :C | |
#{[2 1] [2 2] [2 3]}} ; :B | |
[[:A :A :s :D] [:s :A :A :D] [:C :C :C :C] [:F :F :F :D]] | |
#{#{[0 0] [0 1] [1 1] [1 2]} ; :A | |
#{[0 2]} ; :s | |
#{[0 3] [1 3]} ; :D | |
#{[1 0]} ; :s | |
#{[2 0] [2 1] [2 2] [2 3]} ; :C | |
#{[3 0] [3 1] [3 2]} ; :F | |
#{[3 3]}})) ; :D | |
(with-test | |
(defn- erase [b s n] | |
(->> (grouped-indices b) | |
(remove #(= (get-in b (first %)) s)) | |
(filter #(< n (count %))) | |
(apply union) | |
(reduce #(assoc-in %1 %2 s) (mapv vec b)))) | |
(are [b s n a] (= (erase b s n) a) | |
[[:A :s :s :s] [:A :s :A :A] [:A :A :C :B] [:C :B :B :B]] :s 3 | |
[[:s :s :s :s] [:s :s :A :A] [:s :s :C :s] [:C :s :s :s]] | |
[[:A :A :s :D] [:s :A :A :D] [:C :C :C :C] [:F :F :F :D]] :s 3 | |
[[:s :s :s :D] [:s :s :s :D] [:s :s :s :s] [:F :F :F :D]])) | |
(with-test | |
(defn- step | |
"fall or erase" | |
[b s n] | |
(let [c (fall-one b s)] | |
(if (not= c b) c (erase b s n)))) | |
(are [b s n r] (= (step b s n) r) | |
[[:A :s :s :D] [:A :B :s :s] [:s :s :C :s]] :s 3 | |
[[:s :s :s :s] [:A :s :s :D] [:A :B :C :s]] | |
[[:A :s :C :s] [:s :D :A :D] [:B :C :s :s]] :s 3 | |
[[:s :s :s :s] [:A :D :C :s] [:B :C :A :D]] | |
[[:A :s :s :s] [:A :s :A :A] [:A :A :C :B] [:C :B :B :B]] :s 3 | |
[[:s :s :s :s] [:s :s :A :A] [:s :s :C :s] [:C :s :s :s]] | |
[[:A :A :s :D] [:s :A :A :D] [:C :C :C :C] [:F :F :F :D]] :s 3 | |
[[:s :A :s :D] [:A :A :A :D] [:C :C :C :C] [:F :F :F :D]])) | |
(defn- bprint [b] | |
(print "\033[0;0H") ; move (0,0) | |
(dorun | |
(map (fn [l] | |
(println | |
(apply str | |
(map #({\R "\033[0;31mR\033[0m" | |
\G "\033[0;32mG\033[0m" | |
\B "\033[0;34mB\033[0m" | |
\Y "\033[0;33mY\033[0m"} | |
% %) | |
l)))) | |
b))) | |
(defn- stage [b w] | |
(print "\033[2J") ; clear | |
(loop [b b] | |
(bprint b) | |
(Thread/sleep w) | |
(let [a (mapv #(apply str %) (step (mapv vec b) \space 3))] | |
(if (not= a b) | |
(recur a))))) | |
(defn from-file | |
([f w] (stage (string/split (slurp f) #"\n") w)) | |
([f] (from-file f 500))) | |
(if-let [f (first *command-line-args*)] | |
(from-file f)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
YGYRR | |
R YGYG | |
GYGYRR | |
RYGYRG | |
YGYRYG | |
GYRYRG | |
YGYRYR | |
YGYRYR | |
YRRGRG | |
RYGYGG | |
GRYGYR | |
GRYGYR | |
GRYGYR |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
ぷよぷよ連鎖
随分前の記事ですが http://okajima.air-nifty.com/b/2011/01/2011-ffac.html の問題2が面白そうだったので clojure でやってみました.
java -cp /path/to/clojure.jar clojure.main puyo.clj seq19.dat
で19連鎖の確認ができます.
動作の様子: http://www.youtube.com/watch?v=NwXH8vbAfY8
(clojure になじみの無い方へ.
http://clojure.org/downloads から
clojure-1.5.1.zip をダウンロードして展開し,
上記 /path/to/clojure.jar のところを, 展開した中に入っている
clojure-1.5.1.jar のパスで置き換えてください.
Java の実行環境はあるものとします.)
ぷよが落ちて来て始まるように, データはちょっといじっています.
以下, 解説です.
構成
汎用的な関数は
のような書き方をしています.
テストの書き方はいろいろあります.
環境やライブラリへの依存が少ないと思われる関数のテストを
with-test
を使って,関数の直後に定義するのは単に私の好みであり, 他の方法もあります.
fall-one
行列 b とある要素 s が与えられた時,
全てのセルについて要素が s でなく (e とします).
1行下の要素が s ならば, そのセルの要素を s とし,
1行下の要素を e とします.
他のセルの移動であるセルが s になる場合は,
その上のセルの要素も下に移動します.
要素の型は問いません.
テストの例で言うと,
が与えられた時に s のセルには上から要素が落ちて来て
を返すようにします.
connect
要素のベクタ ev, 集合の集合 ss および要素 e が与えられた時に,
ss の中で ev に含まれる要素を持つような集合を全て連結し,
その中に e を追加します.
#{}
は集合で表示上の要素の順序は無意味です.テストの例で動作を説明しますと,
が与えられた場合,
:a
または:c
を含む集合,#{:a :b}
と#{:c}
を連結し, これに
:g
を追加してを返します.
が与えられた場合,
:a
または:b
を含む集合は#{:a :b}
だけですので,これに
:g
を追加してを返します.
grouped-indices
行列が与えられたとき,
縦または横に等しい要素が並んでいたらこれらを全て連結し,
[行番号, 列番号] の形で表される各座標の集合の集合を返します.
テストの例でいうと
が与えられた場合
のようにグループにできますので, 座標の集合の集合
を返します.
erase
行列 b, ある要素 s, 数 n が与えられた時,
縦横に同じ要素が連接するものを全て結合してグループとした場合,
s 以外の要素からなるグループで要素数が n を越えるグループについて
要素を s に置き換えます.
テストにある例で言うと,
が与えられた時,
の用にグループ分けできますので, :s で無い 3 を越える要素数の
グループ内の要素を全て :s で置き換え,
すなわち
を返します.
条件を満たしたグループの要素を s で置き換えるので,
「s 以外の要素からなるグループ」という条件はあっても無くても同じ結果ですが,
条件を判定するコストと,
全ての s からなるグループの要素をあらためて s で上書きするコストの
うち後者の方が大きいと判断し, 条件を追加することにしました.
step
行列 b, 要素 s, 数 n について, fall-one を適用して変更があった
場合, その行列を, 変更が無かった場合は erase を適用した結果を
返します.
テストの例では
に対しては, fall-one を適用した結果
となり, これは元の b と変更されているので, これが結果となります.
に対しては, fall-one を適用した結果が b と変わらないため,
erase が適用され,
が返ります.
コードの解説は割愛します.
入出力の形式への依存
これ以後のコードは,
とすること
などに依存していますが,
これより前のコードは, そういった詳細に依存していません.
副作用を伴わないコードですので, テストが簡単にかけますし,
要素の型も特定していませんので,
入出力時に扱う要素の表現が変わっても機能します.
例えばなんらかのグラフィック表示で出力を行う場合でも, 再利用できます.
bprint
文字列のシーケンス b が与えられた場合に,
端末の左上から各文字列を1行ずつ表示します.
その際, 端末のエスケープシーケンスが使えれば,
文字
R
は赤,G
は緑,B
は青,Y
は黄色で表示します.端末の色表示をカスタマイズされている場合はこの限りではありません.
コードの説明は割愛しますが
map
の中の副作用が遅延しないよう,dorun
で囲んでいることだけ注記しておきます.stage
盤の初期状態を表す文字列のシーケンス b と, ミリ秒単位の次のステップまでの待ち時間 w が与えられた場合に,
端末のクリアした後,
をループします.
一段階の処理の結果が, 処理前と変わらない場合はループを終了します.
step に渡す際に, 文字列のシーケンス b を,
で文字のベクタのベクタに変換し,
step
によって処理された結果のベクタのベクタを
で文字列のベクタにしています.
from-file
ファイル名 f と待ち時間 w を与えると,
ファイルから文字列を読み出し
stage
を実行します.待ち時間は省略でき, デフォルトは 500 ミリ秒です.
if-let
ロードされた場合にもし引数があれば,
その最初の引数がファイル名であるとして,
待ち時間を指定せずに from-file を呼び出します.