プログラミングの初歩で作りそうなじゃんけんゲームを作る。ただし、PureScriptで作る。

方針

  • Jankenというモジュールを作る
    • グー・チョキ・パーをHandとして定義する
    • じゃんけんの勝負の結果をJudgementとして定義する
    • コンピュータが出す手はランダムに出したいので、ランダムな手を出す関数randomを作っておく
    • 入力は文字列にしたいので、文字列から手に変換する関数fromStringを作っておく
  • 入出力はMainに書く。Node.ReadLineモジュールの力で入力を受け付ける。

準備

適当なプロジェクトディレクトリを作っておいて、

$ spago init

/src/Main.purs/src/Janken.pursを作っておく。

/src/Main.pursはとりあえず以下のようにしておく。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
module Main where

import Prelude

import Effect (Effect)
import Effect.Console (log)

main :: Effect Unit
main = do
  log "Hello"

次のコマンドでHelloが出力されることを確認する。

$ spago run

Jankenモジュールの定義

この節では/src/Janken.pursを編集していく。

1
2
3
module Janken where

import Prelude

Handの定義

じゃんけんの手を表す型Handを定義する。

1
data Hand = Rock | Scissors | Paper

余談。これは公式ではタグ付き共用体と呼ばれているもの。Haskellでは代数的データ型と呼ばれているが、正直名前はどうでもいい。データをこのように表現すれば、「データはこの値しかとりえない」という制限が得られる。制限があれば、プログラムのバグも減らせる。たとえば、「グーを0、チョキを1、パーを2」として表現すると、万が一それ以外の値が来た場合に困る。上のようなHandの定義では、「それ以外の値」が入る余地すら与えない。…この話は、Elm Guideの受け売り。

Judgementの定義

同じようにして、じゃんけんの勝敗を表す型Judgementを定義する。

1
Judgement = WinLeft | WinRight | Draw

なぜWinとかLoseではないのかというと、これはjudge関数の都合である。Judgeは、2つの手を引数にとり、その勝負結果を返す。WinLoseだと、どっちが勝ちでどっちが負けか分からない。なので、「judgeの左側の引数が勝ったらWinLeft、右側が勝ったらWinRight、引き分けならDraw」と定義している。

1
2
3
4
5
6
7
8
judge :: Hand -> Hand -> Judgement
judge Rock Rock = Draw
judge Scissors Scissors = Draw
judge Papser Paper = Draw
judge Rock Scissors = WinLeft
judge Scissors Paper = WinLeft
judge Paper Rock = WinLeft
judge _ _ = WinRight

REPLで遊ぶ

REPLでテストしてみたい。Showクラスのインスタンスにすることで、REPLで値が出力できるようになる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
data Hand = Rock | Scissors | Paper

-- 追加
instance showHand :: Show Hand where
  show Rock = "Rock"
  show Scissors = "Scissors"
  show Paper = "Paper"

data Judgement = WinLeft | WinRight | Draw

-- 追加
instance showJudgement :: Show Judgement where
  show WinLeft = "WinLeft"
  show WinRight = "WinRight"
  show Draw = "Draw"
$ spago repl
> import Janken
> judge Rock Rock
Draw

> judge Rock Paper
WinRight

> judge Rock Scissors
WinLeft

ランダムに出す手の定義

まずは乱数を扱えるパッケージを導入する。

$ spago install random

モジュールを読み込み、randomを定義する。

乱数は副作用付きなので、Effect Hand型を返す。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import Effect (Effect)
import Effect.Random (randomInt) as Random

...

random :: Effect Hand
random = do
  n <- Random.randomInt 0 2
  case n of
       0 -> pure Rock
       1 -> pure Scissors
       _ -> pure Paper

文字列 → 手に変換する関数の定義

Rock、Scissors、Paper以外の値が入力されたら変換に失敗するため、関数の型はMaybe Handである。なので、Maybeが入ったパッケージを導入する。

$ spago install maybe
1
2
3
4
5
6
7
8
9
import Data.Maybe (Maybe(Just, Nothing))

...

fromString :: String -> Maybe Hand
fromString "Rock" = Just Rock
fromString "Scissors" = Just Scissors
fromString "Paper" = Just Paper
fromString _ = Nothing

REPLで遊んでみる。

> import Prelude
> import Janken
> judge Rock <$> fromInt "Rock"
(Just Draw)

> judge Rock <$> fromInt "Scissors"
(Just WinLeft)

> judge Rock <$> fromInt "Paper"
(Just WinRight)

> judge Rock <$> fromInt "aaa"
Nothing

> judge Rock <$> fromInt "hoge"
Nothing

入出力インターフェースの作成

この節では、/src/Main.pursを編集していく。

まずreadlineが使えるパッケージを導入する。

$ spago install node-readline

このパッケージにはNode.jsreadlineをPureScript用にラッピングしただけので、使い勝手はそれと似ている。

使う流れとしては、

  1. createConsoleInterfaceでCUIの入力を受け付けるインターフェースを作る
  2. setLineHandlerで、入力が確定されたときのコールバック関数を指定する。

だけ。だけなのだが、入力が不正だった場合は再度入力を促すようにするので、少しコードが複雑になる。

import文の追加

とりあえずこれだけ書いておく。

1
2
3
4
import Janken as Janken 
import Janken (Judgement(WinLeft, WinRight, Draw), Hand)
import Data.Maybe (Maybe(Just, Nothing))
import Node.ReadLine as NR

インターフェースの作成

createConsoleInterfaceで、コンソール用のインターフェースを作成する。引数には入力補完のための設定を入れるのだが、詳細はNode.ReadLineのドキュメントreadlineのドキュメントを参照。今回は補完は必要ないので、noCompletionを指定している。

runGameは次で作る。

1
2
3
4
main :: Effect Unit
main = do
  interface <- NR.createConsoleInterface NR.noCompletion
  runGame interface

入力処理の作成

runGemeでは、入力を促し、それに応じて処理する機構を書く。

setLineHandlerで、指定されたインターフェースに入力を促す。入力した文字列はhandlerに回され、処理される。promptでプロンプトを出力する。プロンプトの内容はsetPromptで設定できる。

handlerでは、まず入力文字列が正しいものかを判定する。正しかったら、相手の手をランダムに作って、判定を行う。closeでインターフェースを閉じる。もし入力が正しくなかったら、setLineHandlerを再び呼んで再度入力を促す。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
runGame :: NR.Interface -> Effect Unit
runGame interface = do
  let handler :: String -> Effect Unit
      handler input = 
        case Janken.fromString input of
          Just yourHand -> do
            computerHand <- Janken.random
            printJudgement yourHand computerHand
            NR.close interface
          Nothing -> do
            log "Type Rock, Scissors, or Paper."
            NR.setLineHandler interface handler
            NR.prompt interface
  NR.setPrompt "> " 2 interface
  NR.prompt interface
  NR.setLineHandler interface handler

printJudgementでは、じゃんけんの勝敗を出力する。

1
2
3
4
5
6
7
8
printJudgement :: Hand -> Hand -> Effect Unit
printJudgement yourHand computerHand = do
  log $ "You: " <> show yourHand
  log $ "Computer: " <> show computerHand
  case Janken.judge yourHand computerHand of
      WinLeft -> log "You win!"
      WinRight -> log "You lose."
      Draw -> log "Draw."

完成

$ spago run
> hoge
Type Rock, Scissors, or Paper.
> Rock
You: Rock
Computer: Scissors
You win!

感想

PureScriptを書く良い練習になった。

JankenではなくJanken.HandJanken.Judgementというモジュールに分割すべきか、と悩んだ。そうすれば、Janken.fromStringではなくてJanken.Hand.fromStringと書けて、より意味が明らかになる。ただ、そこまで大きなコードではないのでまとめてしまった。

今回Node.ReadLineモジュールを使ったが、そもそもNode.jsのreadlineを使ったことがなかった。調べてなんとかなった。