LoginSignup
10
2

More than 3 years have passed since last update.

Elmでスライドパズル

Last updated at Posted at 2020-12-12

0. スライドパズル

今日はスライドパズルをつくります。
だいたい170行程度のシンプルなやつです。

ss0.gif
ソースコード

elm-animatorを使っていますが, 簡単な使い方などは 「elm-animatorでアニメーション(Animator.Inline)」 をどうぞ。

1. プロジェクトを作成

利用するパッケージをインストールして, src/Main.elm でインポートしましょう。

% elm init
% elm install elm/random
% elm install elm/time
% elm install elm-community/list-extra
% elm install mdgriffith/elm-animator
Main.elm
module Main exposing (main)

import Animator
import Animator.Inline as Inline
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (style)
import Html.Events as Events
import List.Extra as List
import Random
import Time exposing (Posix)

2. データ構造

パズルの盤面を List Int として扱います。
elm-animatorで利用したいので, List IntAnimator.Timeline で包んでおきましょう。

Main.elm
-- モデル
type alias Model = Animator.Timeline (List Int)

3. 乱数でステージを初期化する

パズルなので最初にランダムに盤面をシャッフルして問題を作る必要がありますね。
空白のセル(0)を上下左右のいずれかのセルと入れ替えることを繰り返してシャッフルすることにしましょう。

今回は init-1, +1, -3, +3 のいずれかの要素を24個持つリストを Random.generate で生成します。
つまり, 0のセルと上下左右のセルを24回入れ替えるためのデータです。

Main.elm
type Msg = Shuffle (List Int)

default : List Int
default = List.range 0 8

init : () -> ( Model, Cmd Msg )
init _ =
    ( Animator.init default
    , Random.uniform 1 [ -1, -3, 3 ]
        |> Random.list 24
        |> Random.generate Shuffle
    )

生成したデータを元にセルを入れ替えていく関数を定義しましょう。
簡単のため, 今回は引数に現在の0のインデックスを与えています。 List.Extra.swapAt を使えばリストの要素の入れ替えは楽々です。

Main.elm
shuffle : Int -> List Int -> List Int -> List Int
shuffle zero dirs nums =
    case dirs of
        [] ->
            nums

        x :: xs ->
            let
                next =
                    zero + x
            in
            if 0 <= next && next <= 8 then
                List.swapAt zero next nums
                    |> shuffle next xs

            else
                shuffle zero xs nums

Random.generateupdateで生成したランダムなデータを受け取る必要があるので, update関数を定義します。

Main.elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Shuffle nexts ->
            let
                stage =
                    model
                        |> Animator.current
                        |> shuffle 0 nexts
                        |> Debug.log "model"
            in
            ( Animator.go Animator.immediately stage model
            , Cmd.none
            )

まだ, mainsubscriptionsviewを定義していなかったので, 追加しておきましょう。

Main.elm
main : Program () Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }

subscriptions : Model -> Sub Msg
subscriptions model = Sub.none

view : Model -> Html Msg
view model = div [] []

そうしたら,実行です。

% elm reactor

で localhost:8000 とかにアクセスして,Main.elmを開き,F12キーなどでブラウザの開発者ツールを起動すると,ランダムにシャッフルされた盤面の内容がコンソールに表示されているはずです。
(参考までに,ここまでのソースコードはこちら)

Main.elm:523 model: [3,1,2,0,6,4,5,7,8]

[4,3,5,1,6,0,2,7,8] のようにランダムにシャッフルされた数字のリストが確認できましたね。
これでパズルの盤面に最初に表示するデータを作成できました!

4. アニメーションの設定をする

今回は, パズルのセルがスライドする際にアニメーションするようにしていますので, その処理を追加しましょう。
このあたりは, ほとんどそのまま 「elm-animatorでアニメーション(Animator.Inline)」 の内容です。

Main.elm
 type Msg
     = Shuffle (List Int)
+    | Tick Posix 
Main.elm
-- Model更新用関数(ほぼテンプレート)
animator : Animator.Animator Model
animator =
    Animator.animator
        |> Animator.watching identity always

-- subscriptionで一定時間ごとにTickを呼び出すようにする
subscriptions : Model -> Sub Msg
subscriptions model =
    Animator.toSubscription Tick model animator

-- 一定時間ごとにModelを更新する
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Tick posix ->
            ( Animator.update posix animator model, Cmd.none )

5. 盤面を描画する

Main.elm
view : Model -> Html Msg
view model =
    div
        [ style "width" "300px"
        , style "margin" "auto"
        , style "padding" "100px 0"
        ]
        [ div [] (List.map (cell model) (Animator.current model))
        ]

そして盤面の個々のセルを描画する関数を定義しましょう。
0は空欄なので div [] [] を返し, それ以外ならオレンジ色のセルを返します。

Main.elm
cell : Animator.Timeline (List Int) -> Int -> Html Msg
cell timeline value =
    case value of
        0 ->
            div [] []

        _ ->
            div
                [ style "position" "absolute"
                , Inline.xy timeline <|
                    \xs ->
                        let
                            i =
                                List.elemIndex value xs
                                    |> Maybe.withDefault 0
                        in
                        { x = Animator.at (toFloat (100 * remainderBy 3 i))
                        , y = Animator.at (toFloat (100 * (i // 3)))
                        }
                , style "background-color" "orange"
                , style "width" "99px"
                , style "height" "99px"
                ]
                [ text (String.fromInt value) ]

重要なのは Inline.xy timeline <|... の箇所です。
Animator.Inline.xy を使うことで, セルを表示したい座標と現在のセルの座標が異なる場合にcssアニメーションによってスライドするようにしています。

リロードする度に3x3のシャッフルされたパズルが表示されていますね。
ソースコード

6. クリックしてセルをスライドさせる

オレンジ色のセルをクリックしたら, それを update 関数で検知できるようにしましょう。
まずは Msg型 を修正します。

Main.elm
 type Msg
     = Shuffle (List Int)
     | Tick Posix
+    | Click Int 

そして, セルをクリックした際にイベントが呼び出されるようにします。
先ほどの cell 関数に手を加えましょう。

Main.elm
  _ ->
      div
          [ style "position" "absolute"
+         , Events.onClick (Click value)
          , Inline.xy timeline <|

スライドの処理は, クリックされたセルと空白(0)のセルが隣り合っている場合は, それらを入れ替えることで実現します。
リスト上のクリックされたセルと0のセルのインデックスを取得し, 差の絶対値が1または3である場合はスワップしましょう。

Main.elm
    Click n ->
            let
                clicked =
                    List.elemIndex n (Animator.current model)
                        |> Maybe.withDefault -1

                blank =
                    List.elemIndex 0 (Animator.current model)
                        |> Maybe.withDefault -1

                distance =
                    abs (clicked - blank)

                next =
                    if distance == 1 || distance == 3 then
                        List.swapAt clicked blank (Animator.current model)

                    else
                        Animator.current model
            in
            ( Animator.go Animator.slowly next model
            , Cmd.none
            )

Modelさえ更新すれば, elm-animatorがスライドアニメーションをやってくれます。

ソースコード

7. クリア判定する

クリア条件は現在の盤面が default の値と同じならば 0〜8 まで整列していることになるのでチェックは簡単ですね。

Main.elm
if Animator.current model == default then
    -- クリアしている
else
    -- クリアしていない

今回はシンプルに, クリアしていたら "SOLVED!" と表示するようにしましょう。
view関数に以下のdivを追加してください。

Main.elm
  [ 
+     div
+         [ style "height" "64px"
+         , style "text-align" "center"
+         , style "font-size" "32px"
+         ]
+         [ if Animator.current model == default then
+               text "SOLVED!"
+           else
+               text ""
+           ]
        , div [] (List.map (cell model) (Animator.current model))
        ]

さあ, これで完成です!

ソースコード

elm-animatorを使うことで, Modelは盤面の状態だけしかもっていなくても比較的簡単にスライドアニメーションを追加することができました。
パズルのことだけを考えていたら, 盤面の状態において本質的でないけれどもあったほうが絶対に嬉しいアニメーションが実現できるなんてお得ですね。

10
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
2