LoginSignup
15
8

More than 3 years have passed since last update.

PureScriptでビンゴしようよ!!!!

Last updated at Posted at 2019-05-26

はじめに

こちらのrubyでビンゴしようよ!!!!の記事が面白かったので、これを参考に、PureScriptでビンゴゲームを作ってみました。

ビンゴゲームを作るという課題は、元々はブログ記事でRuby初心者向けに提案されていました。課題の内容はRubyに限定したものではなく一般的なものなので、いろいろな言語で挑戦してみると面白いのではないかと思います。また、リンク先のブログ記事では、他にも課題があげられていたので、興味のある方は腕試しにぜひ挑戦されてみると面白いと思います!

設計

課題自体はシンプルなので、いろいろ作り方はあると思うのですが、せっかくなので、なるべく見通しの良い作りにしたいと思いました。
Rubyの元の記事のコメントを読むと、MVCというか、ビンゴカードを表現するモデルのロジックと、その表示処理を分離して作ることが推奨されています。
これをPureScriptに当てはめた場合、そもそも表示処理はEffectになるので、わりと自然に分離した作りになるように思うのですが、それだけでなく、乱数の発生もEffectになるため、全体的に綺麗な作りになる気がします。

PureScript自体については、こちらの記事がとても参考になります。

マスの扱い

ビンゴのマスには1〜75までの数字が入りますが、中央だけはFreeになっています。そこで、マスを表現するデータ構造としてBoxを導入しました。

data Box = BoxFree | BoxInt Int

Maybeと同じ構造なのですが、Maybeとは区別してShowクラスのインスタンスにしたいため、このようにしました。

instance showBox :: Show Box where
  show (BoxInt v) = show v
  show BoxFree = ""

カードの扱い

カードは単純に、Boxを5x5に並べたものとしました。(定義では、5というサイズはなくて、単なる配列の配列ですが)

type BingoCard = Array (Array Box)

乱数の処理

ビンゴカードを作る場合、例えば1列目を作るには、1〜15の数を作成して、そこからランダムに5つ取り出す必要があります。Rubyでは、Arrayクラスのオブジェクトにsomeという便利なメソッドがあるようなのですが、PureScriptはないようなので、次の二段階の方法で作成することにしました。

  • 配列をシャッフルする関数 shuffle
shuffle :: forall a. Array a -> Effect (Array a)
shuffle input = do
  pairs <- traverse (\i -> (numWithRandom (length input) i)) input
  let shuffledPairs = sortWith _.second pairs
  pure $ _.first <$> shuffledPairs
  where
  numWithRandom :: forall a. Int -> a -> Effect { first :: a, second :: Int }
  numWithRandom len i = do
    r <- randomInt 1 (len*10)
    pure $ { first: i, second: r }

任意の型aの配列をランダムに並び替えます。アルゴリズムとしては、各要素に対してRandomな自然数を割り当て、その値でソートしているだけです。ランダムの値が重複しづらい方がよいので、配列の長さの10倍の値で乱数を発生させています。乱数を使っているので、戻り値はEffect (Array a)型です。

これを使えば、1〜15の数の中から5つ取り出すのは、take 5 $ shuffle (1..15) のように表現出来ます。

実装

メイン処理

メインとなる実装部分は、ビンゴカードを作り出すgenerateCard関数です。

generateCard :: Effect BingoCard
generateCard = do
  card' <- sequence $ map (\x -> do
    let nums = range (x*15+1) (x*15+15)
    randNums5 <- map (map BoxInt) $ take 5 <$> shuffle nums
    pure 
      if (x == 2)
      then
        case updateAt 2 BoxFree randNums5 of
        Just randNums5' -> randNums5'
        Nothing -> randNums5
      else 
        randNums5
  ) (0..4)
  pure $ transpose card'

randNum5は2次元配列で、1行目に1〜15の中からランダムに5つ選んだ配列、2行目に16〜30の中からランダムに5つ選んだ配列、、、、。という感じに格納されます。

中心部分をBoxFreeとするために、updateAtを使っています。このupdateAtは配列の特定の要素を更新する関数ですが、結果がMaybeなので、case文で取り出しています。この条件では、Nothingは呼ばれることはないのですが、ダミーとして何も更新しない配列を返しています。

また、最後にtransposeで行と列を入れ替えています。これも、Rubyでは配列にそういったメソッドがあるようなのですが、PureScriptではないようなので、別途実装しています。

表示処理

表示もちょっと悩みました。Rubyのように文字列を整形する命令があまりないようなので、rjustという補助関数(rjust nで、長さnになるように空白を加える関数。コード全体に記載)を用意して、次のように実装しました。

showCard :: BingoCard -> String
showCard card = intercalate "\n\n" $ (intercalate "|") <<< (map $ (rjust 3) <<< show ) <$> card

intercalateは他の言語でいうjoinみたいなもので、セパレータ文字列を与えて、文字列の配列をくっつけてくれます。cardが2次元配列なので、mapを2回使って(一つは明示的なmap、もう一つは<$>)で、内部のBoxにアクセスして、合成関数(rjust 3) <<< showを適用しています。

コード全体

補助関数も含めたコード全体は次のようになりました。(PureScript 0.12)

module Bingo where

import Prelude
import Effect
import Effect.Console (log)
import Effect.Random
import Data.Maybe
import Data.Array
import Data.Traversable
import Data.Ordering
import Data.String as S

shuffle :: forall a. Array a -> Effect (Array a)
shuffle input = do
  pairs <- traverse (\i -> (numWithRandom (length input) i)) input
  let shuffledPairs = sortWith _.second pairs
  pure $ _.first <$> shuffledPairs
  where
  numWithRandom :: forall a. Int -> a -> Effect { first :: a, second :: Int }
  numWithRandom len i = do
    r <- randomInt 1 (len*10)
    pure $ { first: i, second: r }

transpose :: forall a. Array (Array a) -> Array (Array a) -- assuming all rows are same length.
transpose matrix =
  case (sequence $ map head matrix) of
    Just y -> case (sequence $ map tail matrix) of
      Just ys -> y : (transpose ys)
      Nothing -> []
    Nothing -> []

space :: Int -> String
space 0 = ""
space n = " " <> space (n-1)

rjust :: Int -> String -> String
rjust max str =
  if max < S.length str 
  then str 
  else (space (max - S.length str) ) <> str

data Box = BoxFree | BoxInt Int

instance showBox :: Show Box where
  show (BoxInt v) = show v
  show BoxFree = ""

type BingoCard = Array (Array Box)

generateCard :: Effect BingoCard
generateCard = do
  card' <- sequence $ map (\x -> do
    let nums = range (x*15+1) (x*15+15)
    randNums5 <- map (map BoxInt) $ take 5 <$> shuffle nums
    pure 
      if (x == 2)
      then
        case updateAt 2 BoxFree randNums5 of
        Just randNums5' -> randNums5'
        Nothing -> randNums5
      else 
        randNums5
  ) (0..4)
  pure $ transpose card'

showCard :: BingoCard -> String
showCard card = intercalate "\n\n" $ (intercalate "|") <<< (map $ (rjust 3) <<< show ) <$> card

main :: Effect Unit
main = do
  card <- generateCard
  log "  B|  I|  N|  G|  O\n"
  log $ showCard card

結果

実際に実行してみると、次のような結果になりました。

  B|  I|  N|  G|  O

 15| 21| 39| 48| 66

  8| 26| 43| 53| 65

  2| 19|   | 59| 67

  7| 27| 44| 49| 72

  6| 20| 35| 52| 69

終わりに

やはりRubyのように便利なメソッドやライブラリがたくさん用意されているのは何かと便利ですね。頑張って、PureScript用に汎用的に使えるライブラリを作りたいなと思いました。

shuffleを実装する中で、Haskellであればタプルを使うような処理の部分も、レコード型にしました。PureScriptにもTuple型はあるので、最初はそれを使っていたのですが、PureScriptはレコード型が便利で積極的にそれを使っていく方がPureScriptらしいので、レコード型に書き換えました。

細かいことですが、rjustを作るときに、第一引数を文字列にするか、長さにするか悩みました。オブジェクト指向言語のオブジェクト(Rubyでいうレシーバ)は、関数型言語でいうと第一引数に割り当てるのが良さそうな気がするのですが、それだとクロージャとしてrjust nのように使えないので、ちょっと不便ではないかと思い、第一引数を文字列の長さにしてみました。

設計自体は、関数型言語での設計がまだよくわかっていませんが、データ構造を作り、なるべく汎用的に使える関数を作って組み合わせていく、、、ということを意識してみたのですが、なかなかまだ感覚がつかめていないです。ぜひ、アドバイスなどコメントをいただけたらと思います。

15
8
3

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
15
8