[Elm] 懐かしのモン○ターファーム2のアルバイトを作ってみた

公開:2020/09/22
更新:2020/09/22
8 min読了の目安(約4800字TECH技術記事

Image from Gyazo

最近、スマートフォンや家庭用ゲーム機で移植版が発売されたモン○ターファーム2が発売されましたね!本編もすんごく懐かしくてハマりにハマった記憶がありますが、ポケステでプレイできたアルバイトというミニゲームも懐かしさがありますよね。ということで記念にElmで作ってみました!(移植版でもプレイできるようです。)

こちらのオンライン開発環境でプレイ・変更ができます! ※PC専用です

ミニゲームの内容は、いわゆる脳トレみたいな内容で2つの数値がランダムに表示され、数値の大きさによって矢印キーを押してその正誤によってハイスコアを目指すゲームです。以下ルールです。

上キー: 2つの数値の合計が10
左右キー: 2つの数値のうち大きい方数値
下キー: 2つの数値が同じ(合計が10になる5, 5を除く)

アプリケーションで使用される状態(Model)です。全て、整数で構成されていてシンプルで良いですね。

  • スコア(score)
  • 2つの数値(numbers)
  • 制限時間(time)
  • キーを押して得られたポイント(acquiredPoint)
type alias Numbers =
    ( Int, Int )


type alias Model =
    { score : Int, numbers : Numbers, time : Int, acquiredPoint : Maybe Int }

画面表示の全てです(view)。時間のメーター、2の数値、スコア等を表示しています。これらの表示にはModelが使われます。

view : Model -> Html Msg
view model =
    let
        ( n1, n2 ) =
            model.numbers

        high =
            Attr.attribute "high"

        low =
            Attr.attribute "low"

        optimum =
            Attr.attribute "optimum"

        acquiredPointText =
            Maybe.withDefault "" <|
                Maybe.map
                    (\ap ->
                        (if ap > 0 then
                            "+"

                         else
                            ""
                        )
                            ++ String.fromInt ap
                    )
                    model.acquiredPoint
    in
    main_ []
        [ meter [ Attr.min "0", Attr.max "60", low "20", high "40", optimum "0", value <| String.fromInt <| 60 - model.time ] []
        , p [] [ text <| String.fromInt n1 ++ " " ++ String.fromInt n2 ]
        , p [] [ text <| "score: " ++ String.fromInt model.score ++ acquiredPointText ]
        ]

初期値(処理)です。制限時間が60(秒)でセットされ、2の数値を乱数で初期化します。

init : () -> ( Model, Cmd Msg )
init _ =
    ( { score = 0, numbers = ( 0, 0 ), time = 60, acquiredPoint = Nothing }, Random.generate GotNumbers twoNumbers )

twoNumbers : Random.Generator Numbers
twoNumbers =
    Random.pair (Random.int 0 9) (Random.int 0 9)

グローバルで監視されるイベントです(subscriptions)。制限時間を数えるために1秒毎にTickメッセージを発行。矢印キーを押されたときにPressArrowKeyメッセージを発行しています。onKeyDownイベントをフックし、"event.key"を取得し独自の型Directionに変換してメッセージに乗せます。

subscriptions : Model -> Sub Msg
subscriptions { time } =
    Sub.batch <|
        if time > 0 then
            [ onKeyDown (Decode.map PressArrowKey keyDecoder)
            , Time.every 1000 Tick
            ]

        else
            -- Game Over
            []
						
						
type Direction
    = Left
    | Right
    | Up
    | Down
						
						
toDirection : String -> Decoder Direction
toDirection string =
    case string of
        "ArrowLeft" ->
            Decode.succeed Left

        "ArrowRight" ->
            Decode.succeed Right

        "ArrowUp" ->
            Decode.succeed Up

        "ArrowDown" ->
            Decode.succeed Down

        _ ->
            Decode.fail "key is not arrow key."


keyDecoder : Decode.Decoder Direction
keyDecoder =
    Decode.andThen toDirection (Decode.field "key" Decode.string)

メッセージ(Msg)の定義とメッセージを受信した時の処理(update)です。矢印キーが押されたら、数値と方向を渡して獲得ポイントの算出、スコアに反映、獲得ポイントのセットをしています。最後に乱数を発生させます。発生させた乱数はModelのnumbersにセットされます。1秒おきに制限時間を減らし、獲得ポイントをリセットしています。

type Msg
    = PressArrowKey Direction
    | GotNumbers Numbers
    | Tick Time.Posix


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        PressArrowKey direction ->
            let
                acquiredPoint =
                    calcPoint model.numbers direction
            in
            ( { model | score = model.score + acquiredPoint, acquiredPoint = Just acquiredPoint }, Random.generate GotNumbers twoNumbers )

        GotNumbers numbers ->
            ( { model | numbers = numbers }, Cmd.none )

        Tick _ ->
            ( { model | time = model.time - 1, acquiredPoint = Nothing }, Cmd.none )


calcPoint : Numbers -> Direction -> Int
calcPoint ( n1, n2 ) direction =
    let
        sum =
            n1 + n2

        direction2Point =
            case direction of
                Up ->
                    10

                Down ->
                    5

                Left ->
                    1

                Right ->
                    1
    in
    if sum == 10 && direction == Up then
        direction2Point

    else if n1 == n2 && direction == Down then
        direction2Point

    else if n1 > n2 && sum /= 10 && direction == Left then
        direction2Point

    else if n1 < n2 &&  sum /= 10 && direction == Right then
        direction2Point

    else
        negate <| direction2Point

以上がコードの解説となります。なんとなくElmのコードてこんな感じなんだなあ。ちょっと触ってみようかなあ。移植されたゲーム懐かしい or 知らないからやってみようかなあ。そんな気持ちになっていただければうれしいです!