Chapter 06

碁盤を表示する

tkzwhr
tkzwhr
2023.03.21に更新

ここからはユーザーインターフェースの実装を行っていきます。

ElmではElmアーキテクチャというものに則って実装していきます。

https://guide.elm-lang.jp/architecture/

少し難しいのですが、今のところは

  • ページはどんなデータを持つのか(Model)
  • ページのデータはどう変化させていくのか(Update)
  • ページのデータはどう表示されるのか(View)

の3つの仕組みを考えて作れば、後はElmがいい感じにしてくれる、くらいの理解で良いかと思います。

まずは、下記のページで紹介されている範囲で、碁盤の表示を行ってみます。

https://guide.elm-lang.jp/architecture/text_fields.html

別途、Wikimedia Commonsにある碁盤と碁石のパーツ(SVG画像)をダウンロードして、assets/imagesに配置しておきます。

https://commons.wikimedia.org/wiki/Category:Go_(set_of_square_images)?uselang=ja

今回のソースコード
src/Main.elm
module Main exposing (main)

import Browser
import Html exposing (Html, div, img)
import Html.Attributes exposing (..)


main =
    Browser.sandbox { init = init, update = update, view = view }



-- Constants


constants =
    { cellSize = 50 }



-- Type Aliases


-- ★ポイント①
type alias Model =
    { boardSize : Int }


type alias Coord =
    ( Int, Int )



-- Models


init : Model
init =
    { boardSize = 9 }



-- Updates


update : msg -> Model -> Model
update _ _ =
    init



-- Views


view : Model -> Html msg
view model =
    goBoardView model.boardSize


goBoardView : Int -> Html msg
goBoardView boardSize =
    div
        [ style "display" "grid"
        , style "grid-template-columns" ("repeat(" ++ (boardSize |> String.fromInt) ++ ", " ++ (constants.cellSize |> String.fromInt) ++ "px)")
        , style "grid-auto-rows" ((constants.cellSize |> String.fromInt) ++ "px")
        ]
        (gen2DCoordList boardSize
            |> List.map (cellView 9)
        )


cellView : Int -> Coord -> Html msg
cellView boardSize coord =
    img [ src ("assets/images/Go_" ++ toGoBoardPiece boardSize coord ++ ".svg"), width constants.cellSize ] []



-- View Logics


-- ★ポイント②
gen2DCoordList : Int -> List Coord
gen2DCoordList size =
    List.range 1 size
        |> List.map
            (\r ->
                List.range 1 size
                    |> List.map
                        (\c ->
                            ( r, c )
                        )
            )
        |> List.reverse
        |> List.concat


toGoBoardPiece : Int -> Coord -> String
toGoBoardPiece size ( r, c ) =
    let
        rStr =
            if r == 1 then
                "d"

            else if r == size then
                "u"

            else
                "c"

        cStr =
            if c == 1 then
                "l"

            else if c == size then
                "r"

            else
                "c"
    in
    rStr ++ cStr

以下、ポイントです。

碁盤の大きさ

囲碁には9×9の9路盤、13×13の13路盤、19×19の19路盤(一般的に使われるサイズ)など、様々な大きさの碁盤があります。後々、ユーザーが好きな大きさを選べるようにするために、碁盤の大きさをModelとして定義して、初期値として9を入れておきます。

type alias Model =
    { boardSize : Int }

init : Model
init =
    { boardSize = 9 }

後々、この碁盤の大きさをユーザーが変えられるようにします。

Viewの書き方

普通はWebサイトはHTMLタグを組み合わせて書きますが、 ElmではHTMLタグに対応する関数が用意されているので、それを組み合わせて書きます。

a [ href "https://google.co.jp", target "_blank" ] [ text "Google" ]
-- <a href="https://google.co.jp" target="_blank">Google</a>

HTMLタグがネストされると、大量に[]が出現するので、読むのに少し慣れが必要です。

div [ style "background-color" "green" ] [ ol [] [ li [] [ text "Item 1" ], li [] [ text "Item 2" ] ] ]

碁盤のレイアウト

碁盤をHTMLでどう構成するかについては、いくつか方法が考えられそうです。今回はCSS Gridを使って碁盤のマスをn×nで並べてみます。

(以下は4×4の碁盤の例)

<div style="display: grid; grid-template-columns: repeat(4, 50px); grid-auto-rows: 50px">
    <div>(左上のSVG)</div>
    <div>(上のSVG)</div>
    <div>(上のSVG)</div>
    <div>(右上のSVG)</div>

    <div>(左のSVG)</div>
    <div>(中央のSVG)</div>
    <div>(中央のSVG)</div>
    <div>(右のSVG)</div>

    <div>(左のSVG)</div>
    <div>(中央のSVG)</div>
    <div>(中央のSVG)</div>
    <div>(右のSVG)</div>

    <div>(左下のSVG)</div>
    <div>(下のSVG)</div>
    <div>(下のSVG)</div>
    <div>(右下のSVG)</div>
</div>

碁盤の大きさが決まっていれば上のようにも書けるのですが、後々碁盤の大きさが変わることを念頭に置くと、CSS Gridの中のdiv要素はプログラムで生成する必要があります。

方法としては、一例として

  1. (0, 0)(n, n)までの座標リストを生成する
  2. (r, c)rn→0になるように並び替える(左下が(0, 0)になるようにするためで、これは次のページで説明します)
  3. 座標の位置によって9パターンのSVGのいずれかを使うIMGタグを生成する

でできそうです。

Elmではどう書けば良いか、Listのリファレンスページを見ながらElm REPLを使って検証してみます。

$ elm repl
---- Elm 0.19.1 ----------------------------------------------------------------
Say :help for help and :exit to exit! More at <https://elm-lang.org/0.19.1/repl>
--------------------------------------------------------------------------------
> 
> List.range 0 3 |>
| List.map (\i -> List.range 0 3 |> List.map (\j -> (i, j) ) ) |>
| List.reverse
[[(3,0),(3,1),(3,2),(3,3)],[(2,0),(2,1),(2,2),(2,3)],[(1,0),(1,1),(1,2),(1,3)],[(0,0),(0,1),(0,2),(0,3)]]
    : List (List ( Int, Int ))
>
> List.range 0 3 |>
| List.map (\i -> List.range 0 3 |> List.map (\j -> (i, j) ) ) |>
| List.reverse |>
| List.concat
|
[(3,0),(3,1),(3,2),(3,3),(2,0),(2,1),(2,2),(2,3),(1,0),(1,1),(1,2),(1,3),(0,0),(0,1),(0,2),(0,3)]
    : List ( Int, Int )
>
> List.range 0 3 |>
| List.map (\i -> List.range 0 3 |> List.map (\j -> (i, j) ) ) |>
| List.reverse |>
| List.concat |>
| List.map (\(i, j) -> case (i, j) of
| (3, 0) -> "左上"
| (3, 3) -> "右上"
| (0, 0) -> "左下"
| (0, 3) -> "右下"
| (3, _) -> "上"
| (0, _) -> "下"
| (_, 0) -> "左"
| (_, 3) -> "右"
| (_, _) -> "中央"
| )
|
["左上","上","上","右上","左","中央","中央","右","左","中央","中央","右","左下","下","下","右下"]
    : List String

ということで、range、map、reverse、concatあたりを使うことで目的を達成できそうなことがわかりました。

起動してみる

Main.elmをコンパイルしてブラウザで表示してみましょう。

$ $ elm make src/Main.elm
Success!     

    Main ───> index.html

$ elm reactor --port 3001
Go to http://localhost:3001 to see your project dashboard.

ブラウザを起動し、 http://localhost:3001/index.html にアクセスします。

クライアントアプリの起動1

碁盤が表示されました!

次は、子の碁盤に碁石を表示できるようにしていきます。