なのログ

だから見てて下さい・・・俺の・・・変身

ぐだぐだクソコード vol.1 はじめてのテトリス by F#

こんにちはこんばんは。

皆様お待たせ致しました。ぐだぐだクソコードの時間です。
(※元ネタです: http://www.hareruyamtg.com/article/category/detail/218


諸般の事情によりテトリスを作ることとなり、
あろうことかF#で挑んできました、という回です。

いや、どうやらちゃんとした人たちからすれば
ちゃんとしたコードを書けるらしい >Tetris | F# Snippets んですが、

長らく自分の頭で考える事の無いコーディングしかしないクソ作業員であった僕は
やはり(プログラマとして)ちゃんとした人などでは全くなかったらしく、
書きはしたもののとんでもねえクソコードを生み出す結果になってしまったため、
反省と精進のため、ここに公開セルフコードレビュー記事を起こそう、となった次第です。

f:id:nanoyatsu:20180918230632p:plain
図1.「とりあえず動いたよ」アピに必死ななのやつ氏


というわけでコードレビューですが、とりあえず記事末尾にコード全文を掲載しています。
各自このページを2枚のウィンドウで開いたりエディタにコピペしたりして、
記事と横並びにして見やすいようにして御覧ください。
このブログにそれ以上に気の利いた機能はありません。フォントも非等角です。

あるいは、F#とXAMLWPFが出来る環境を立ててビルドすればお楽しいただけるかと思います。
そんな環境がある人が何人いるのかは全く不明です。 (実はVisualStudioだけあればすぐできるんですけどね)

では、はじめていきましょう。


~~記事をほぼ起こしてから書いたエリア~~(なかなか始まらずすみません)

ぶっちゃけ、セルフコードレビューとは言うものの、
正直なところ「自覚しているダメポイントを自分からイイワケして、公開する上での衝撃を和らげよう」
の文章であることが間違いありませんでした。自白。

本当にダセエなというところですが、まあ今能力が足りないことは仕方がないなと思って、
ブログタイトルの所にある「だから見ててください・・・俺の・・・変身」を主題に思うこととして、 前を向いていきたいと思います。

型システム入門も読み進めつつ、
これをリファクタリングしていくのも追ってやっていきたいところです。

それでは改めて、進めていきます。↓

~~あとから書いたエリアおわり~~


  • 定数定義とか
// ミノの定義
let T = true
let F = false
let MINO_I_MAP:bool[,] = array2D [[F;F;F;F;F];[F;F;F;F;F];[F;T;T;T;T];[F;F;F;F;F];[F;F;F;F;F]]
let MINO_L_MAP:bool[,] = array2D [[F;F;F;F;F];[F;F;F;T;F];[F;T;T;T;F];[F;F;F;F;F];[F;F;F;F;F]]
        //~~略~~
//初期座標
let NEXT_MINO_POS = (-1,3)
let START_MINO_POS = (1,3)
        //~~略~~
//他の記号(マップチップ・・・?)
let MAPCHIP_EMPTY = ' '
let MAPCHIP_ERASE = '※'

このへんですね。
いきなりTとかFとかオリジナリティ出してきましたが、
これはもうわかりませんでした。データをコード上に持つって何。(問題を大きくして誤魔化すプレイング)

いわゆる「アルゴリズムとデータ構造」のデータ構造の部分な訳ですが、
今回は全部Array2Dに入れました。
これによってこの後色々と犠牲が生まれますが、 格子状のデータだしこれはこれで妥当ではあると思っています。

また、Array2Dについて、走査順がよく解っていなかったのでこれまであまりArray2D.iterとかを使ったことがなかったんですが、
今回試した所ずいぶん素直な子だったので、どんどこ多用しております。

MINO_POSたちは初期座標で、とりあえず定義している分ちゃんと使ったんですが、
TF並びのやつと合わせてやっと意味がある感じなので、
なんとなくよろしくなさを醸し出している定数です。

ちなみにミノというのはテトリミノのことです。テトリスのピースはテトリミノというらしい。


  • DEFAULT_FIELDくんちょっときて
let DEFAULT_FIELD = "\
            \n\
NEXT        \n\
┏━━      ━━┓\n\
┃          ┃\n\
┃          ┃\n\
        //~~略~~
┃          ┃\n\
┗━━━━━━━━━━┛"

うーん、さっきミノで見た。
ソース上にこういうのをベタで書くのほんとどうなんですかね(同じ話題)。
データに落とすに当たっては、そこそこうまく(後述)やっているつもりなんですけど。


  • コメントから声が聞こえる
// TODO: 構造化する(まるでイメージできていない)
[<STAThread>]
[<EntryPoint>]
let main argv = 

・・・せやな・・・。 (それじゃあがんばってイメージしような!の意)


  • イカれたメンバーを紹介するぜ!
    // ゲーム画面状態 座標は[y,x]になる 行列の添字と同じ
    let mutable mainField:char[,] = array2D (seq {for s in DEFAULT_FIELD.Split('\n') ->
                                                  seq {for c in s -> c}
                                            })
    // 操作中のテトリミノ、座標、NEXTのところに出るテトリミノ
    let mutable currentMino:char[,] = Array2D.create 5 5 MAPCHIP_EMPTY
    let mutable minoPosY,minoPosX = START_MINO_POS
    let mutable nextMino:char[,] = Array2D.create 5 5 MAPCHIP_EMPTY
    
    let mutable prevTime = DateTime.Now
    let rnd = new Random()
    view.lblMainField.Content <- DEFAULT_FIELD

まずmutableなゲーム画面配列!
次にmutableな今のミノ!
それからmutableな操作中のミノの座標!
もうひとりmutableな次のミノ!
そしておまけにタイマーもmutable!

以上だ!

C#でやれ。

・・・つらいですが、「良いトコ」探しをするとしましょう。mainFieldの定義にかろうじてそれらしさがある気がします。
二次元に展開される文字列を、二重のシーケンス式を使って見事にArray2Dに格納しています。
F#スゲエ!って言える気がしてきました。どうですか。チョースゲエでしょう。みなさんもさあ。チョースゲエ!アッコさんスゲエ!(本心でない発言の例)

ちなみにですが、mainFieldと今&次テトリミノをそれぞれ別で持っているので、
描画の時は3枚重ねて貼り付ける、みたいなことをしています。
今ミノが着地したらmainFieldに印刷される、みたいな感じです。
このせいで発生した面倒もある感じですが、最適な実装というのがいまだにわかっていません。


  • なんか関数たち
    // nextMinoPosを作る
    let newMinoGenerate (rnd:Random) (minoMap:(bool[,]*char)[]) =
        let katati,moji = minoMap.[minoMap |> Array.length |> rnd.Next]
        katati |> Array2D.map (fun x -> if x then moji else MAPCHIP_EMPTY)

    // 正方配列左90度回転
    let rotateAry ary =
        let size = (Array2D.length1 ary)
        let rotated = Array2D.zeroCreate size size
        Array2D.iteri (fun y x v -> (rotated.[(size-1)-x,y] <- v)) ary
        rotated

・・・このへんぶっちゃけ普通なんですよね・・・
なんかちゃんと副作用とか意識してるし・・・

ただこう、節々賢くない気はしています。
特に後者の回転の方ですが、今回のコード、再帰を1回も使ってないんですよね。
「このへん再帰チャンスだろ本当に関数型やる気あんのかよ」みたいな。再帰したがりファンクショナルキッズ。

ただこう、どちらにしても、「そもそも全部配列にしちゃったもんな」みたいな。配列が悪い。(決めたのは自分)


  • そして背景が置き去りになる
    // 行揃った判定
    let eraseCheck (mainField:char[,]) =
        let minoArea = Array2D.create 20 10 ' '
        Array2D.blit mainField 3 1 minoArea 0 0 20 10

        //~~略~~

    // ※※の行を消す
    let lineErase (mainField:char[,]) =
        let minoArea = Array2D.create 21 10 ' '
        Array2D.blit mainField 3 1 minoArea 1 0 20 10

        //~~略~~

 は ? 

突然のマジックナンバーの嵐。

これまで散々言った""データの持ち方"" とは。(1 0 20 10 ←らへんに対して言っています)

いやその、ここに関しては釈明があるんです。ただの言い訳タイムですけど。
このあたり、もうほとんど最後の最後に作ってた箇所なんですよ。

したらもう、ふっと「(前と違う)XXなやり方で解決してえなあ」と思っても、
「前部分直すのしんどくねえ・・・?大丈夫・・・?」となるわけです。

うむ、しゃーない。(本当ごめんなさい実はこの作業タイムリミットもあって・・・)

というわけで、まあ「後から直す」前提な訳ですが、
これに関しては、ココに合わせてまわりを修正することになるかなあ、とか思っています。
いやぶっちゃけそのほうが絶対素直なコードになるんですよね・・・節々が・・・。

なぜ初めこの形にした・・・

設計、大事ッスね。(強引なシメ)


  • 高階関数とか抽象化とかが出てきそうなところ
    // ぶつかり確認 操作前とか移動前とかにやる
    let collisionCheck (mainField:char[,]) (mino:char[,]) (minoPos:int*int) =
        let mutable result = Some (mino,minoPos)
        let collisionCheck' (offsetY,offsetX) idxY idxX elem =
            let y,x = offsetY+idxY,offsetX+idxX
            if isIndexInArray mainField.[*,0] y &&
               isIndexInArray mainField.[0,*] x &&
               (elem <> MAPCHIP_EMPTY) &&
               mainField.[y,x] <> MAPCHIP_EMPTY
            then result <- None

        Array2D.iteri (collisionCheck' minoPos) mino
        result

    // 動いてるミノと動いてない画面を重ねる
    let projectionMino (mainField:char[,]) (mino:char[,]) (minoPos:(int*int)) =
        let projectedField = Array2D.rebase mainField
        let projectionAry (offsetY,offsetX) idxY idxX elem =
            let y,x = offsetY+idxY,offsetX+idxX
            if isIndexInArray projectedField.[*,0] y && 
               isIndexInArray projectedField.[0,*] x && 
               elem <> ' '
            then projectedField.[y,x] <- elem

このへん、
「なんか抽象化してまとめれそうじゃない・・・?どう・・・?」
という嗅覚に引っかかって仕方がありません。考えねばならない。

ほとんど完全にその場凌ぎ的にoptionとかし始めてるし。
設計力・・・。(完全に書きながら考えているツケ)


    // char[,]を改行付きStringにする
    let generatePrintString (sourceField:char[,]) =
        let mutable printString = ""
        for i in 0..(Array2D.length1 sourceField)-1 do
            sourceField.[i,*] |> Array.iter (fun c -> (printString <- printString + (c|>string)))
            printString <- printString + ("\n")
        done
        printString.Trim('\n')

    // 背景と今minoと次minoを重ねて描画するやつ(中でgeneratePrintStringしている)
    let gamePrint mainField currentMino currentPos nextMino =
        let currentProjected = projectionMino mainField currentMino currentPos
        let bothProjected = projectionMino currentProjected nextMino NEXT_MINO_POS
        view.lblMainField.Content <- (generatePrintString bothProjected)

ここのmutableもなんとかなるでしょコレ???と思っています。

あと2つめの関数のほうは、
引数の位置の都合とかでパイプラインしにくかったり、
その状態でかっこ使ってゴリ押しワンラインするでもないなと思ったりして letletしてしまっているので、そこも考え直してキレイに書けそうな気がしています。

  • そして最大の問題児現る
    // 進むやつ
    let gameLoop = async {
        while true do
            let now = DateTime.Now
            let dt = now - prevTime

            if dt.TotalSeconds > 0.8
            then
                printfn "prev: %A, now: %A,dt: %A" prevTime now dt.TotalSeconds
                prevTime <- now
                
                mainField <- lineErase mainField
                match collisionCheck mainField currentMino (minoPosY+1,minoPosX) with
                | Some (m,(y,x)) -> minoPosY <- y
                | None -> mainField <- projectionMino mainField currentMino (minoPosY,minoPosX)
                          if minoPosY = fst START_MINO_POS
                          then
                          //このへんもう力尽きてなんの保守性もない
                            Array.iteri (fun x v -> mainField.[11,x] <- v) [|'※';'※';'G';'A';'M';'E';'O';'V';'E';'R';'※';'※';|]
                          else
                            mainField <- eraseCheck mainField
                            currentMino <- nextMino
                            minoPosY <- fst START_MINO_POS 
                            minoPosX <- snd START_MINO_POS
                            nextMino <- newMinoGenerate rnd MINO_MAP

                view.Dispatcher.Invoke(fun () -> gamePrint mainField currentMino (minoPosY,minoPosX) nextMino)
        done
        }

このへんに対して言いたいことは色々あるんですけど、
とりあえずコレを見てほしいんですよね。

visualstudioの編集画面のキャプチャなんですけど。
f:id:nanoyatsu:20180919002239p:plain

よくわからないと思うので、上の方の行列回転させる関数らへんとかも撮りますよ。
f:id:nanoyatsu:20180919002512p:plain

上のやつ、なんかすごい黄色だ。
これが何を表すかというと、mutableな値のシンタックスハイライトですね。

ワーオ。イキイキ。

これがなんとかなるかはもう、僕にはわかりませんね・・・。
がんばりますとしか。がんばりますとしか言えねえよお・・・。

あと、ここで初めてF#での非同期処理を使いました。今回とかじゃなくて。僕史上初。
それもあり、ぶっちゃけまるで理解が足りていません。
Async.Startしたらバックグラウンドで動くんやぞ。以上や。(ひでえ)

また、今回やったようなループも再帰で書けるようなんですが、それもまた課題です。

        done
        }

    // ループ発火
    Async.Start gameLoop

    // ここからイベント    
    view.btnStart.Click.Add(fun _ ->

現状、ガチの無限ループしてますからね・・・


  • あとは消化試合みたいなもので
    // ここからイベント    
    view.btnStart.Click.Add(fun _ ->
        mainField <- array2D (seq {for s in DEFAULT_FIELD.Split('\n') ->
                                   seq {for c in s -> c}
                             })
        currentMino <- newMinoGenerate rnd MINO_MAP
        minoPosY <- fst START_MINO_POS 
        minoPosX <- snd START_MINO_POS
        nextMino <- newMinoGenerate rnd MINO_MAP
        gamePrint mainField currentMino (minoPosY,minoPosX) nextMino
    )

    //キー入力のイベント
    view.KeyDown.Add(fun arg ->
        match arg.Key with
        | Key.Up    -> match collisionCheck mainField (rotateAry currentMino) (minoPosY,minoPosX) with
                        | Some (m,(y,x)) -> currentMino <- m
                        | None -> printfn("collision")
        | Key.Left  -> match collisionCheck mainField currentMino (minoPosY,minoPosX-1) with
                        | Some (m,(y,x)) -> minoPosX <- x
                        | None -> printfn("collision")
        | Key.Right -> match collisionCheck mainField currentMino (minoPosY,minoPosX+1) with
                        | Some (m,(y,x)) -> minoPosX <- x
                        | None -> printfn("collision")
        | Key.Down  -> while (collisionCheck mainField currentMino (minoPosY+1,minoPosX) <> None) do
                        minoPosY <- minoPosY+1
                       done
        | _ -> printfn("non effect key")
        
        gamePrint mainField currentMino (minoPosY,minoPosX) nextMino
    )

ちょろっと操作用のイベント処理が載っています。
多少キツめのmatchネスト、match不完全を回避するためだけのNone分岐、
そしてその先の無意味printfが目を引くところですが、もはや話題ではないのではないか。

いや間違いなくこれも課題なんですけどね。どうすればいいんですかこういうの。
単に「何もしない」とかありましたっけ・・・? ignore・・・?みたいな気持ち。



  • お疲れ様でした

長々と書きました。
見苦しかった方はすみません・・・。

見立てが立っている訳ではないですが、
これを「なんかエエ感じやん!?」となるようにリファクタリングするまでが
今回のテトリス編だと思っています。なんとかしたいと思っています。←できませんでした(けど本当にどこかではやりたい)

それでは、最後にコード全文を貼り付けて終わります。
F#部分全部で254行でした。冒頭に貼った模範解答みたいのが216行。 自分のはえげつないStringとかコメントとかがあるので、結局規模は似た感じなのかもしれません。

また、お気づきかと思いますが、ここまでのコードブロックでは、
(これまで積み重ねた言い訳と似たような内容の)コメントを抜いてあります。

ああ、あと、(なかなか終わらんな) 画面の右側、「スコアとか消したラインの数とか出したいね」という気持ちがあったんですが、 色々カツカツだったりして見送ってしまいました。どうせなら次はそれも載せたいところですね。

それではまた。次回はたぶん型システム入門のほうです。
お付き合い頂きありがとうございました。お疲れ様でした。

module App

open System
open FsXaml
open System.Windows
open System.Windows.Input

type MainView = XAML<"MainWindow.xaml">

// ミノの定義 T,Fは趣味
let T = true
let F = false
let MINO_I_MAP:bool[,] = array2D [[F;F;F;F;F];[F;F;F;F;F];[F;T;T;T;T];[F;F;F;F;F];[F;F;F;F;F]]
let MINO_L_MAP:bool[,] = array2D [[F;F;F;F;F];[F;F;F;T;F];[F;T;T;T;F];[F;F;F;F;F];[F;F;F;F;F]]
let MINO_J_MAP:bool[,] = array2D [[F;F;F;F;F];[F;T;F;F;F];[F;T;T;T;F];[F;F;F;F;F];[F;F;F;F;F]]
let MINO_Z_MAP:bool[,] = array2D [[F;F;F;F;F];[F;T;T;F;F];[F;F;T;T;F];[F;F;F;F;F];[F;F;F;F;F]]
let MINO_S_MAP:bool[,] = array2D [[F;F;F;F;F];[F;F;T;T;F];[F;T;T;F;F];[F;F;F;F;F];[F;F;F;F;F]]
let MINO_T_MAP:bool[,] = array2D [[F;F;F;F;F];[F;F;T;F;F];[F;T;T;T;F];[F;F;F;F;F];[F;F;F;F;F]]
let MINO_O_MAP:bool[,] = array2D [[F;F;F;F];[F;T;T;F];[F;T;T;F];[F;F;F;F]]
let MINO_MAP = [|MINO_I_MAP,'■';MINO_L_MAP,'▲';MINO_J_MAP,'△';MINO_Z_MAP,'★';MINO_S_MAP,'☆';MINO_T_MAP,'◆';MINO_O_MAP,'□'|]
// ↑のミノのメモ
//I          L          J          Z          S          T          O
//□□□□□ □□□□□ □□□□□ □□□□□ □□□□□ □□□□□ □□□□
//□□□□□ □□□■□ □■□□□ □■■□□ □□■■□ □□■□□ □■■□
//□■■■■ □■■■□ □■■■□ □□■■□ □■■□□ □■■■□ □■■□
//□□□□□ □□□□□ □□□□□ □□□□□ □□□□□ □□□□□ □□□□
//□□□□□ □□□□□ □□□□□ □□□□□ □□□□□ □□□□□ 

//初期座標
let NEXT_MINO_POS = (-1,3)
let START_MINO_POS = (1,3)
let DEFAULT_FIELD = "\
            \n\
NEXT        \n\
┏━━      ━━┓\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┃          ┃\n\
┗━━━━━━━━━━┛"

//他の記号(マップチップ・・・?)
let MAPCHIP_EMPTY = ' '
let MAPCHIP_ERASE = '※'


// TODO: 構造化する(まるでイメージできていない)
[<STAThread>]
[<EntryPoint>]
let main argv = 
    let view = MainView()
    
    // ゲーム画面状態 座標は[y,x]になる 行列の添字と同じ
    let mutable mainField:char[,] = array2D (seq {for s in DEFAULT_FIELD.Split('\n') ->
                                                  seq {for c in s -> c}
                                            })
    // 操作中のテトリミノ、座標、NEXTのところに出るテトリミノ
    let mutable currentMino:char[,] = Array2D.create 5 5 MAPCHIP_EMPTY
    let mutable minoPosY,minoPosX = START_MINO_POS
    let mutable nextMino:char[,] = Array2D.create 5 5 MAPCHIP_EMPTY
    
    let mutable prevTime = DateTime.Now
    let rnd = new Random()

    // 初期処理
    view.lblMainField.Content <- DEFAULT_FIELD


    //領域外判定
    let isIndexInArray ary idx = (0 <= idx) && (idx <= ((Array.length ary)-1))

    // nextMinoPosを作る
    let newMinoGenerate (rnd:Random) (minoMap:(bool[,]*char)[]) =
        let katati,moji = minoMap.[minoMap |> Array.length |> rnd.Next]
        katati |> Array2D.map (fun x -> if x then moji else MAPCHIP_EMPTY)

    // 正方配列左90度回転
    let rotateAry ary =
        let size = (Array2D.length1 ary)
        let rotated = Array2D.zeroCreate size size
        Array2D.iteri (fun y x v -> (rotated.[(size-1)-x,y] <- v)) ary
        rotated

    // 行揃った判定
    let eraseCheck (mainField:char[,]) =
        // TODO: 唐突に背景抜きの20*10の扱いを始めてしまう 他と処理方法をあわせる(たぶん他を合わせる方向のがよい)
        // TODO: 20,10とか3,1とか、当然ベタで書くべきではない
        let minoArea = Array2D.create 20 10 ' '
        Array2D.blit mainField 3 1 minoArea 0 0 20 10
        //Array2D.iteri (fun y x v -> minoArea.[y,x] <- mainField.[y+3,x+1]) minoArea //blitにした
        
        for y in 0..(Array2D.length1 minoArea)-1 do
            match Array.tryFind (fun v -> v = MAPCHIP_EMPTY) minoArea.[y,*] with
            | Some _ -> printfn "eraseCheck Some %s %A" (y.ToString()) minoArea.[y,*]
            | None -> Array.iteri (fun x v -> (mainField.[y+3,x+1] <- MAPCHIP_ERASE)) minoArea.[y,*]
        done
        
        mainField //直接※つっこんだ blitやめた

    // ※※の行を消す
    let lineErase (mainField:char[,]) =
    // REVIEW: なんか計算量多い気がする(雰囲気でものを言いがち)
    // > 実際、※がタテに続いているところがあったらそこはいっきに潰せる。
        let minoArea = Array2D.create 21 10 ' '
        Array2D.blit mainField 3 1 minoArea 1 0 20 10
        Array2D.iteri (fun y x v -> if v = MAPCHIP_ERASE
                                    then ( for y' in y..(-1)..1 do minoArea.[y',x] <- minoArea.[y'-1,x]
                                    )) minoArea
        Array2D.blit minoArea 1 0 mainField 3 1 20 10
        mainField
    
    // ぶつかり確認 操作前とか移動前とかにやる
    let collisionCheck (mainField:char[,]) (mino:char[,]) (minoPos:int*int) =
        let mutable result = Some (mino,minoPos)
        let collisionCheck' (offsetY,offsetX) idxY idxX elem =
            let y,x = offsetY+idxY,offsetX+idxX
            if isIndexInArray mainField.[*,0] y &&
               isIndexInArray mainField.[0,*] x &&
               (elem <> MAPCHIP_EMPTY) &&
               mainField.[y,x] <> MAPCHIP_EMPTY
            then result <- None

        Array2D.iteri (collisionCheck' minoPos) mino
        result

    // REVIEW: ↑こいつら↓抽象化してまとめれそうすぎる
    // 動いてるミノと動いてない画面を重ねる
    let projectionMino (mainField:char[,]) (mino:char[,]) (minoPos:(int*int)) =
        let projectedField = Array2D.rebase mainField
        let projectionAry (offsetY,offsetX) idxY idxX elem =
            let y,x = offsetY+idxY,offsetX+idxX
            if isIndexInArray projectedField.[*,0] y && 
               isIndexInArray projectedField.[0,*] x && 
               elem <> ' '
            then projectedField.[y,x] <- elem

        Array2D.iteri (projectionAry minoPos) mino
        projectedField

    // char[,]を改行付きStringにする
    let generatePrintString (sourceField:char[,]) =
        // TODO:なんか付け足し付け足しする再帰か何かでimmutableにできるとおもう
        let mutable printString = ""
        for i in 0..(Array2D.length1 sourceField)-1 do
            sourceField.[i,*] |> Array.iter (fun c -> (printString <- printString + (c|>string)))
            printString <- printString + ("\n")
        done
        printString.Trim('\n')

    // 背景と今minoと次minoを重ねて描画するやつ(中でgeneratePrintStringしている)
    let gamePrint mainField currentMino currentPos nextMino =
        let currentProjected = projectionMino mainField currentMino currentPos
        let bothProjected = projectionMino currentProjected nextMino NEXT_MINO_POS
        view.lblMainField.Content <- (generatePrintString bothProjected)

    // 進むやつ
    // TODO: 仕事を分ける
    let gameLoop = async {
        // TODO: こういうループも再帰で書けるらしい
        while true do
            let now = DateTime.Now
            let dt = now - prevTime

            if dt.TotalSeconds > 0.8
            then
                printfn "prev: %A, now: %A,dt: %A" prevTime now dt.TotalSeconds
                prevTime <- now
                
                mainField <- lineErase mainField
                match collisionCheck mainField currentMino (minoPosY+1,minoPosX) with
                | Some (m,(y,x)) -> minoPosY <- y
                | None -> mainField <- projectionMino mainField currentMino (minoPosY,minoPosX)
                          if minoPosY = fst START_MINO_POS
                          then
                          //このへんもう力尽きてなんの保守性もない
                            Array.iteri (fun x v -> mainField.[11,x] <- v) [|'※';'※';'G';'A';'M';'E';'O';'V';'E';'R';'※';'※';|]
                          else
                            mainField <- eraseCheck mainField
                            currentMino <- nextMino
                            minoPosY <- fst START_MINO_POS 
                            minoPosX <- snd START_MINO_POS
                            nextMino <- newMinoGenerate rnd MINO_MAP

                view.Dispatcher.Invoke(fun () -> gamePrint mainField currentMino (minoPosY,minoPosX) nextMino)
        done
        }

    // ループ発火
    Async.Start gameLoop

    // ここからイベント    
    view.btnStart.Click.Add(fun _ ->
        mainField <- array2D (seq {for s in DEFAULT_FIELD.Split('\n') ->
                                   seq {for c in s -> c}
                             })
        currentMino <- newMinoGenerate rnd MINO_MAP
        minoPosY <- fst START_MINO_POS 
        minoPosX <- snd START_MINO_POS
        nextMino <- newMinoGenerate rnd MINO_MAP
        gamePrint mainField currentMino (minoPosY,minoPosX) nextMino
    )

    //キー入力のイベント
    view.KeyDown.Add(fun arg ->
        // TODO:match不完全を避けるためにprintfnしたけど妥当感が全く無い
        match arg.Key with
        | Key.Up    -> match collisionCheck mainField (rotateAry currentMino) (minoPosY,minoPosX) with
                        | Some (m,(y,x)) -> currentMino <- m
                        | None -> printfn("collision")
        | Key.Left  -> match collisionCheck mainField currentMino (minoPosY,minoPosX-1) with
                        | Some (m,(y,x)) -> minoPosX <- x
                        | None -> printfn("collision")
        | Key.Right -> match collisionCheck mainField currentMino (minoPosY,minoPosX+1) with
                        | Some (m,(y,x)) -> minoPosX <- x
                        | None -> printfn("collision")
        | Key.Down  -> while (collisionCheck mainField currentMino (minoPosY+1,minoPosX) <> None) do
                        minoPosY <- minoPosY+1
                       done
        | _ -> printfn("non effect key")
        
        gamePrint mainField currentMino (minoPosY,minoPosX) nextMino
    )

    //// デバッグ用
    //view.btndebug.Click.Add(fun _ ->
    //    currentMino <- nextMino
    //    minoPosY <- 10
    //    minoPosX <- 3
    //    nextMino <- newMinoGenerate rnd MINO_MAP
    //    gamePrint mainField currentMino (minoPosY,minoPosX) nextMino
    //)

    let app = Application()
    app.Run(view)
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="TETRIS" Height="360" Width="360"
    FontFamily="Myrica M">
    <Grid Height="329" VerticalAlignment="Bottom">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>
        <Label x:Name="lblMainField" Grid.Column="0" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Background="Black" Foreground="White" ></Label>
        <!--<Button x:Name="btndebug" Margin="10,10,10,294" Grid.Column="1" >debug</Button>-->
        <Button x:Name="btnStart" Margin="10,264,10,40" Grid.Column="1" >start/reset</Button>
    </Grid>
</Window>