ようこそ。睡眠不足なプログラマのチラ裏です。

とことんF#よぷよ! 第 63 回 CLR/H 勉強会で、F#とXNAを題材に発表します。

F# + XNAでとことんF#よぷよ!してみました




「ダークソウル」で心が折れそうな日々を送っている今日このごろですが、みなさんはいかがお過ごしでしょうか。
F# + XNAで「とことんぷよぷよ」っぽいものを実装してみました(はじめてのXNAゲームプログラミング)。
ただし、意図的、あるいは意図せずに元ネタのそれの仕様とは異なる場合があります。
動画の終盤、PAUSEをしてチート機能を使うことで19連鎖のデモをしています。



第 63 回 CLR/H 勉強会で、F#とXNAを題材にお話します。

日時 : 2011/10/15 (土) 13:30〜18:00(開場 13:00)
場所 : マイクロソフト北海道支店 セミナールーム (札幌市中央区北 5 条西 2 丁目 5 JR タワーオフィスプラザさっぽろ 20F)
      http://www.microsoft.com/ja-jp/mscorp/branch/hokkaido.aspx
参加費:500 円(会場費やお菓子代に使用させて頂きます)


【タイトル】
とことんF#よぷよ! - F# + XNAによるゲームプログラミング入門 -


【概要】
みんな大好き、落ち物パズルゲーム「ぷよぷよ」は今年で 20 周年を迎えました。
ぷよぷよ風落ち物パズルゲームの作り方と、関数型パラダイムを中心としたマルチパラダイム言語 F# による
XNA でのゲーム開発の基本についてお話します。
「ぷよぷよ風落ちゲーを 500 行程度で実装できる。そう、F# + XNA ならね!」


【スピーカー】
わたし


CLR/H 公式サイト
http://clr-h.jp/



また、マイクロソフトエバンジェリストで、著作に「The root of .NET Framework」や「実践 F# 関数型プログラミング入門」などがある
荒井省三さんが、「DLR + ASync + アルファ」というタイトルで濃いセッションをしてくださいます。ぜひお越しください。



事前に「とことんF#よぷよ!」のコードを晒してみる

ということで、F#でXNAなコードを勉強会前に晒してしまう大盤振る舞い(!?)
コードは決してうつくしくはありませんが、興味のある方はごらんください。


PuyoPuyo.fs

namespace PuyoPuyoLibrary
open System
type PuyoColors =
 | n = 0x00 | r = 0x01
 | y = 0x02 | p = 0x03
 | g = 0x04 | b = 0x05

type Union = 
  | None   = 0b0000 | Top    = 0b0001
  | Left   = 0b0010 | Bottom = 0b0100
  | Right  = 0b1000
      
type puyoObj = { position : int * int; pattern : PuyoColors[][]; color1: PuyoColors; color2: PuyoColors; hidden: bool; upside : bool}
type PuyoState = { pw : int; ph : int; width : int; height : int
                   gameover   : bool; pause : bool ;cheat : bool
                   totalScore : decimal; highScore : decimal; maxLevel : int
                   scoreBase  : int; magnifyingPower : int
                   rensa : int; union : int; colors : int; erased : int
                   current    : puyoObj; next : puyoObj array
                   field      : PuyoColors[,];  checkField : bool[,]
                   falling    : bool; allclear   : bool
                   etarget    : (int * int * PuyoColors) list} 

module PuyoPuyo =
  let patterns x y =
    [| [| enum 0;      x; enum 0; |]
       [| enum 0;      y; enum 0; |]
       [| enum 0; enum 0; enum 0; |] |]
  let none : PuyoColors [][] = [| [| |] |]
  let clearCheckField ps = { ps with checkField = Array2D.create ps.width ps.height false }
  let getLevel erased = erased / 40 |> fun x -> if x >= 999 then 999 else x + 1
      
  let create erased =
    let rand = new Random(System.DateTime.Now.Millisecond)
    let create' = fun s e -> enum (rand.Next(s, e)), enum (rand.Next(s, e))
    fun () -> (if getLevel erased < 3 then (1,4) ||> create'
               elif getLevel erased < 5 then (1,5) ||> create'
               else (1,6) ||> create') ||> fun x y -> { position = (1, 0); pattern = patterns x y; color1 = x; color2 = y; hidden = false; upside = true }
  let getPuyoObj = 
    let queue = new System.Collections.Generic.Queue<puyoObj>()
    fun erased -> queue.Count |> function
       | 0 -> seq {1..3} |> Seq.iter (fun x -> queue.Enqueue ( () |> create erased))
              queue.Dequeue(),queue.ToArray()
       | _ -> queue.Enqueue(() |> create erased)
              queue.Dequeue(),queue.ToArray()  
  
  let convert (source : PuyoColors [][]) = 
    (Array.length source, source.GetLength(0)) ||> fun row col -> Array2D.create row col PuyoColors.n |> fun array ->
    if source = none then array
    else seq { for i in 0..row - 1 do
               for j in 0..col - 1 do yield i,j } |> Seq.iter (fun (i,j) -> array.[j,i] <- source.[i].[j])
         array

  type Direction = | Right | Left | Down
  let move ps direction = ps.current.position ||> fun x y -> direction |> function
    | Right -> { ps.current with position = x + 1, y }
    | Left  -> { ps.current with position = x - 1, y }
    | Down  -> { ps.current with position = x    , y + 1 } 

  let descend ps = ps.current.position ||> fun x y -> 
    if y + 1 < ps.height  then { ps.current with position = x, y + 1 }
    else ps.current 

  let rotate puyo action = convert puyo.pattern |> fun pattern ->
    let len = Array2D.length1 pattern 
    seq { for i in 0..len - 1 do
          for j in 0..len - 1 do
          yield i,j,len,pattern} |> Seq.iter action
    puyo

  let avoidance ps exchange (c1,c2) = 
    let (|Insert|_|) c1 c2 ps = ps.current.position ||> fun x y ->
      let judge c f g=
        if (c <> PuyoColors.n && (x < 0 || (x >= 0 && (ps.field.[x,y] <> PuyoColors.n || ps.field.[x,y+1] <> PuyoColors.n)))) &&
           (c <> PuyoColors.n && (x+2 > ps.width - 1 || (x+2 <= ps.width - 1 && (ps.field.[x+2,y] <> PuyoColors.n || ps.field.[x+2,y+1] <> PuyoColors.n)))) then
           f()
        else g() 
      judge c1 (fun () -> Some ps.current) (fun () -> judge c2 (fun () -> Some ps.current) (fun () -> None))
    let (|CollideLeft|_|) c1 c2 ps = ps.current.position ||> fun x y ->  
      if (c1 <> PuyoColors.n || c2 <> PuyoColors.n) && (x < 0 || (x >= 0 && (ps.field.[x,y] <> PuyoColors.n || ps.field.[x,y+1] <> PuyoColors.n ))) then
        Some ps.current 
      else None
    let (|CollideRight|_|) c1 c2 ps = ps.current.position ||> fun x y ->  
      if (c1 <> PuyoColors.n || c2 <> PuyoColors.n) && 
        (x+2 > ps.width - 1 || (x+2 <= ps.width - 1 && (ps.field.[x+2,y] <> PuyoColors.n || ps.field.[x+2,y+1] <> PuyoColors.n))) then
        Some ps.current       
      else None
    ps |> function
    | Insert c1 c2  puyo -> puyo
    | CollideLeft c1 c2 puyo -> move ps Right |> rotate <| exchange
    | CollideRight c1 c2  puyo -> move ps Left |> rotate <| exchange
    | _ -> ps.current |> rotate <| exchange

  let target ps = ps.current.pattern.[2].[1], ps.current.pattern.[0].[1]
  let rotateR ps =
    if ps.current.pattern = none then ps.current 
    else
      (fun (i,j,len,pattern:PuyoColors[,]) -> ps.current.pattern.[i].[j] <- pattern.[i,len - 1 - j])
      |> avoidance ps <| target ps

  let rotateL ps =
    if ps.current.pattern = none then ps.current 
    else
      (fun (i,j,len,pattern:PuyoColors[,]) -> ps.current.pattern.[j].[i] <- pattern.[len - 1 - j,i])
      |> avoidance ps <| target ps

  let createState pw ph width height erased =
    let current,next = getPuyoObj erased
    { pw = pw; ph = ph; width = width; height = height; gameover = false; pause = false; cheat = false;
      totalScore = decimal 0; highScore = decimal 0; maxLevel = 0; scoreBase = 0; magnifyingPower = 0; rensa = 0; union = 0; colors = 0; erased = 0;
      current = current; next = next;  field = Array2D.create width height PuyoColors.n
      checkField = Array2D.create width height false;  falling = false; allclear = false; etarget = List.empty }

  let nextPuyo ps = if not ps.cheat then getPuyoObj ps.erased ||> fun current next -> {ps with current= current; next = next} 
                           else { ps with current = { position = (1, 0); pattern = patterns PuyoColors.p PuyoColors.y; color1 = PuyoColors.p; color2 = PuyoColors.y; hidden = false; upside = true }; next = ps.next }
  let reset ps = nextPuyo { ps with erased = 0 } |> fun ps -> nextPuyo ps |> fun ps -> nextPuyo ps |> fun ps -> 
    { ps with field = Array2D.create ps.width ps.height PuyoColors.n; checkField = Array2D.create ps.width ps.height false; etarget = List.empty 
              gameover = false; pause = false; cheat = false; totalScore = decimal 0; maxLevel = ps.maxLevel; highScore = ps.highScore;
              scoreBase = 0; magnifyingPower = 0; rensa = 0; union = 0; colors = 0; allclear = false }

  let cheat ps = Array2D.create ps.width ps.height PuyoColors.n |> fun newfield ->
    let dic = dict[0,[0;0;0;2;1;1];1,[1;2;0;3;2;3];2,[3;2;3;2;1;1];3,[1;2;3;2;1;3];4,[2;3;2;1;2;3];5,[3;2;1;2;1;3];6,[2;3;2;1;2;1];7,[2;3;2;1;2;1];8,[2;1;1;3;1;3];9,[1;2;3;2;3;3];10,[3;1;2;3;2;1];11,[3;1;2;3;2;1];12,[3;1;2;3;2;1]]
    for y in 0..ps.height - 1 do dic.[y] |> List.iteri (fun x c -> newfield.[x, y] <- enum c)
    nextPuyo { ps with field = newfield; cheat = true } 

  let detectCollision puyo field = 
    let result = ref false
    puyo.pattern |> convert |> Array2D.iteri
      (fun i j c -> c |> function
         | PuyoColors.n -> ()
         | _ -> puyo.position ||> fun x y -> (x + i,y + j) ||> fun xi yj ->
                if xi < 0 || xi >= Array2D.length1 field ||
                   yj < 0 || yj >= Array2D.length2 field ||
                   field.[xi, yj] <> PuyoColors.n then 
                   result := true); !result

  let getAllclearScore ps = if ps.field = Array2D.create ps.width ps.height PuyoColors.n |> not then decimal 0
                            else decimal 3600 + decimal (getLevel ps.erased * 5) 
  let getScore ps =
    let rensaBonus n = dict [1,0;2,8;3,16;4,32;5,64;6,96;7,128;8,160;9,192;10,224;11,256;12,288;13,320;14,352;15,388;16,416;17,448;18,480;19,512] |> fun dic -> if n > 19 then dic.[19]  else dic.[n]
    let unitBounus n = dict [4,0;5,2;6,3;7,4;8,5;9,6;10,7;] |> fun dic -> if n > 10 then 10 else dic.[n]
    let colursBounus n = dict [0,0;1,0;2,3;3,6;4,12;5,24] |> fun dic -> dic.[n]
    let a,b,c,d = ps.union * 10,rensaBonus ps.rensa,unitBounus ps.union,colursBounus ps.colors // 基本得点,連鎖ボーナス,連結ボーナス,複色ボーナス
    b + c + d |> fun x -> if x = 0 then a,1 else a,x

  let fixed' ps = ps.current.pattern |> convert |> fun c ->
    seq { for i in 0..Array2D.length1 c - 1 do 
          for j in 0..Array2D.length2 c - 1 do
          c.[i,j] |> function
          | PuyoColors.n -> ()
          | _ -> ps.current.position ||> fun x y -> ps.field.[x + i, y + j] <- ps.current.pattern.[j].[i] } |> Seq.iter id
    { ps with current = {ps.current with hidden = true }; falling = true }

  let fall ps =
    seq { for x in 0..ps.width-1 do
          for y in [ps.height-1 .. -1 .. 0] do
          if ps.field.[x,y] = PuyoColors.n then
            for z in (y-1) .. -1 .. 0 do                                 
            if ps.field.[x,z] > PuyoColors.n then
              ps.field.[x,z+1] <- ps.field.[x,z]
              ps.field.[x,z] <- PuyoColors.n
              yield z } |> Seq.length 

  let erase ps = 
    let erase' x y ps = 
      if ps.field.[x,y] = PuyoColors.n then None
      else
        let result, list = ref 1, ref [x,y]
        let rec search x y result = 
          ps.checkField.[x,y] <- true
          let search' x y retsult f g predicate = 
            if predicate (f x) (g y) ps && 
                ps.checkField.[f x, g y] <> true &&
                ps.field.[f x, g y] = ps.field.[x,y] then 
              result := !result + 1
              list := !list@[f x, g y] 
              search (f x) (g y) result 
          search' x y result (fun x -> x+1) id (fun x y ps -> x < ps.width)
          search' x y result id (fun y -> y+1) (fun x y ps -> y < ps.height)
          search' x y result (fun x -> x-1) id (fun x y ps -> x > 0)
          search' x y result id (fun y -> y-1) (fun x y ps -> y > 0)
        search x y result
        if !result >= 4 then 
          List.map (fun (x,y) -> x,y,ps.field.[x,y] )!list |> Some
        else None
    seq { for x in 0..ps.width-1 do
          for y in ps.height-1 .. -1 .. 0 do
          let ecount = erase' x y (clearCheckField ps) |> function
            |Some x -> x
            | _ -> []
          yield! ecount } |> Seq.distinct |> Seq.toList 
    |> fun etarget -> let colors = List.map (fun (x,y,_) -> ps.field.[x,y]) etarget |> Seq.distinct |> Seq.toList 
                      etarget,List.length etarget, List.length colors, { ps with current = { ps.current with pattern = none } }

  let getUnion ps x y = 
    if x < 0 || x > ps.width - 1 || y > ps.height - 1 || ps.field.[x,y] = PuyoColors.n then Union.None
    else 
      let rise ps x y =  [y..(ps.height-1)] |> List.exists (fun y -> ps.field.[x,y] = PuyoColors.n)
      let lrunion n b f add = 
        if b || (ps.field.[f x,y] = ps.field.[x,y] &&
                 (y = ps.height - 1 
                 || (y < ps.height - 1 && (rise ps x y |> not && rise ps (f x) y |> not))
                 || (y < ps.height - 1 && (rise ps x y && rise ps (f x) y )))) |> not then n
        else add n
      let left n = lrunion n (x = 0) (fun x -> x - 1) ((+) Union.Left)
      let right n = lrunion n (x >= ps.width - 1) ((+) 1) ((+) Union.Right) 
      let tbunion n f g c = if f || ps.field.[x,g y] = ps.field.[x,y] |> not then n else n + c
      let top n = tbunion n (y = 0) (fun y -> y - 1) Union.Top
      let bottom n = tbunion n (y >= ps.height - 1) (fun y -> y + 1) Union.Bottom
      Union.None |> (top >> left >> right >> bottom)


PuyoGame.fs

namespace FSharpyopuyo
open System
open System.Runtime.Serialization
open Microsoft.Xna.Framework
open Microsoft.Xna.Framework.Audio
open Microsoft.Xna.Framework.Graphics
open Microsoft.Xna.Framework.Input
open Microsoft.Xna.Framework.Storage
open PuyoPuyoLibrary

[<DataContract>]
type SaveGameData = { [<field:DataMember(Name="MaxLevel")>] MaxLevel : int; [<field:DataMember(Name="HighScore")>] HighScore : decimal }

type PuyoGame () as this = 
  inherit Game()
  let gametitle, gmanager, sprite = "とことんF#よぷよ!", new GraphicsDeviceManager(this), lazy new SpriteBatch(this.GraphicsDevice)
  let mutable ps = PuyoPuyo.createState 32 32 6 13 0 
  let slowTimelag,fastTimelag, fps = 350.,230.,60.
  let backgroundTexture,puyoTexture,blinkTexture,gameoverTexture,allclearTexture,carbancleTexture,suketoudaraTexture = 
    ["background";"puyopuyo";"blink";"batanQ";"allclear";"carbuncle";"suketoudara"] 
    |> List.map (fun name -> lazy this.Content.Load<Texture2D>(@"Content\image\" + name)) |> function 
    | a::b::c::d::e::f::g::[] -> a,b,c,d,e,f,g | _ -> invalidArg "tlist" "リストの長さが違うよ。"
  let font = lazy this.Content.Load<SpriteFont>(@"Content\font\SpriteFont1")
  let gameSe = ["move";"rotate";"drop";"batanQ";"allclear";"pafu";] |> List.map 
                (fun name -> this.Content.Load<SoundEffect>(@"Content\sound\" + name).CreateInstance() |> fun x -> x.Volume <- 0.3f; lazy x)
  let chainSe = [1..19] |> List.map (fun i -> this.Content.Load<SoundEffect>(@"Content\sound\chain" + if i > 7  then string 7 else string i).CreateInstance() |> fun x -> x.Volume <- 0.3f; lazy x)
  let chainVoice = [1..19] |> List.map (fun i -> this.Content.Load<SoundEffect>(@"Content\sound\chainvoice" + if i > 5 then string 5 else string i).CreateInstance() |> fun x -> x.Volume <- 0.3f; lazy x)
  let bgm = (this.Content.Load<SoundEffect>(@"Content\sound\MorningOfPuyoPuyo").CreateInstance() |> fun x -> x.Volume <- 0.2f; x.IsLooped <- true; lazy x)

  let soundPlay (se:Lazy<SoundEffectInstance>) = se.Force().Play()
  let checkPauseKey  = 
    let keyWait, pauseKeyDown, pauseTimelag =  ref 0., ref false, 700.
    (fun () -> if not ps.gameover then
                 keyWait := !keyWait + fps
                 if ps.pause && !keyWait > pauseTimelag && Keyboard.GetState().IsKeyDown(Keys.F12) then 
                      ps <- PuyoPuyo.cheat ps;
                 if !keyWait > pauseTimelag && Keyboard.GetState().IsKeyDown(Keys.P) && !pauseKeyDown then 
                   gameSe.[5] |> soundPlay; bgm |> soundPlay; ps <- { ps with pause = false }; pauseKeyDown := false
                 elif !keyWait > pauseTimelag &&  Keyboard.GetState().IsKeyDown(Keys.P) && not !pauseKeyDown then 
                   gameSe.[5] |> soundPlay; bgm.Force().Stop(); ps <- { ps with pause = true }; pauseKeyDown := true 
                 if !keyWait > pauseTimelag then keyWait := 0.)
  let operateKeys = 
    let keyWait = ref 0.
    (fun () -> 
      keyWait := !keyWait + fps
      let operateKey key = 
        if not ps.gameover then
            key |> function
            | Keys.Z | Keys.Space | Keys.NumPad8 when !keyWait > slowTimelag -> 
              gameSe.[1] |> soundPlay; Some (PuyoPuyo.rotateL ps)
            | Keys.X | Keys.Up | Keys.NumPad5 when !keyWait > slowTimelag ->
              gameSe.[1] |> soundPlay; Some (PuyoPuyo.rotateR ps)
            | Keys.Right | Keys.NumPad6  when !keyWait > slowTimelag -> 
              gameSe.[0] |> soundPlay; Some (PuyoPuyo.move ps PuyoPuyo.Right)
            | Keys.Left  | Keys.NumPad4  when !keyWait > slowTimelag -> 
              gameSe.[0] |> soundPlay; Some (PuyoPuyo.move ps PuyoPuyo.Left)
            | Keys.Down  | Keys.NumPad2  when !keyWait > fastTimelag -> 
              if ps.current.hidden |> not then ps <- { ps with totalScore = (+) ps.totalScore <| decimal 1 }
              Some (PuyoPuyo.move ps PuyoPuyo.Down)
            | Keys.Escape -> this.Exit(); None
            | _ -> None
          else
            key |> function
            | Keys.Enter -> ps <- PuyoPuyo.reset ps; bgm |> soundPlay; None
            | Keys.Escape -> this.Exit(); None
            | _ -> None
        |> function 
          | Some x -> if not (PuyoPuyo.detectCollision x ps.field) then
                        ps <- { ps with current = x }
          | None -> ()
      let resetWait () = if !keyWait > slowTimelag then keyWait := 0.
      Keyboard.GetState().GetPressedKeys() |> Array.toList |> List.sort |> List.rev |> function
      | [x] -> operateKey x; resetWait ()
      | [x;y] -> operateKey x; operateKey y; resetWait ()
      | [x;y;z] | x::y::z::_  -> operateKey x; operateKey y; operateKey z; resetWait ()
      | _ -> () ) 

  let blink = let blinkWait, blink = ref 0., ref false
              fun () -> if ps.pause then !blink else
                        blinkWait := !blinkWait + fps
                        if !blinkWait > 5. * fps then blink := not !blink; blinkWait := 0.
                        !blink

  let saveFilename = "puyosys.sav"
  let saveStorage (device:StorageDevice) (level:int) score = 
    let result = device.BeginOpenContainer(gametitle, null, null)
    if result.AsyncWaitHandle.WaitOne() then
      use container = device.EndOpenContainer(result)
      result.AsyncWaitHandle.Close()
      if container <> null then
        if (container.FileExists(saveFilename)) then
          container.DeleteFile(saveFilename)
        use stream = container.CreateFile(saveFilename)
        let serializer = DataContractSerializer(typeof<SaveGameData>)
        serializer.WriteObject(stream , { MaxLevel = level; HighScore = score })
  
  let loadStorage (device:StorageDevice) = 
    let result = device.BeginOpenContainer(gametitle, null, null)
    let level,highscore = ref 1, decimal 0 |> ref
    if result.AsyncWaitHandle.WaitOne() then
      use container = device.EndOpenContainer(result)
      result.AsyncWaitHandle.Close()
      if container <> null && container.FileExists(saveFilename) then
        use stream = container.OpenFile(saveFilename, System.IO.FileMode.Open )
        let serializer = DataContractSerializer(typeof<SaveGameData>)
        let data = serializer.ReadObject(stream) :?> SaveGameData
        level := data.MaxLevel; highscore := data.HighScore 
    !level, !highscore

  let drawPuyo (c:PuyoColors, x, y, ((i, j) as point)) (location:Vector2) rect hw =
    let fx,fy = float32 (x+i), float32 (y+j)
    let lx,ly = float32 location.X, float32 location.Y 
    let draw f = sprite.Force().Draw(puyoTexture.Force(), Vector2(32.f * fx + lx,32.f * fy + ly |> f),  Nullable rect, Color.White)
                 if point = (1,1) then 
                   let texture = if blink () then blinkTexture.Force() else puyoTexture.Force() 
                   sprite.Force().Draw(texture, Vector2(32.f * fx + lx,32.f * fy + ly |> f),  Nullable rect, Color.White)
    c |> function
      | PuyoColors.n -> () 
      | _ ->  if hw && ps.current.upside then (fun x -> x - 16.f) |> draw else draw id 

  let uncoupling c adjustx = Rectangle(0, ps.ph * (int c - 1), ps.pw / adjustx, ps.ph)
  let drawText (msg:string) (v:Vector2) c = sprite.Force() |> function
    | x -> font.Force() |> fun font -> [font, msg, Vector2(v.X+3.f,v.Y+3.f), Color.Black; font, msg, v, c ] 
                                       |> List.iter (fun (font, msg, v, c) ->  x.DrawString(font, msg, v, c))
  let drawLiteral () = 
    ["NEXT",Vector2(240.f, 34.f),Color.MediumSpringGreen; "LEVEL",Vector2(240.f, 322.f),Color.MediumSpringGreen; "SC.",Vector2(32.f, 420.f),Color.HotPink; 
     "HSC.",Vector2(32.f, 2.f),Color.Orange; "MLV.",Vector2(230.f, 2.f),Color.Orange] |> List.iter (fun (s,v,c) -> drawText s v c)
 
  let drawScoreAndLevel () = 
    seq { yield ((PuyoPuyo.getLevel ps.erased |> string).PadLeft(3,'0')), Vector2(255.f, 356.f), Color.HotPink 
          if ps.totalScore < ps.highScore then 
            yield string ps.highScore |> fun s -> s, Vector2(224.f - font.Force().MeasureString(s).X, 2.f), Color.Orange
          else yield string ps.totalScore |> fun s ->  s, Vector2(224.f - font.Force().MeasureString(s).X, 2.f) , Color.Orange
          if ps.etarget = List.empty then 
            yield string ps.totalScore |> fun s -> s, Vector2(224.f - font.Force().MeasureString(s).X, 420.f), Color.HotPink
          else yield string ps.scoreBase + "×" + (string ps.magnifyingPower).PadLeft(4,' ') 
                     |> fun s -> s, Vector2(224.f - font.Force().MeasureString(s).X, 420.f), Color.SkyBlue 
          if (PuyoPuyo.getLevel ps.erased) < ps.maxLevel then
            yield (string ps.maxLevel).PadLeft(3,'0') , Vector2(280.f , 2.f), Color.Orange
          else yield (PuyoPuyo.getLevel ps.erased |> string).PadLeft(3,'0') , Vector2(280.f , 2.f), Color.Orange }
    |> Seq.iter (fun (s,v,c) -> drawText s v c)

  let drawField = 
    let gameoverWait = ref 0.
    (fun location () ->
    let unit ps x y (c:PuyoColors) adjustx = 
      PuyoPuyo.getUnion ps x y |> fun r -> Convert.ToInt32(r |> int |> string) 
      |> fun x -> Rectangle(ps.pw * x,ps.ph * (int c - 1), ps.pw / adjustx, ps.ph)
    ps.field |> Array2D.iteri (fun i j c -> if j > 0 then drawPuyo(c, i, j, (0, 0)) location (unit ps i j c 1) false)
    if ps.gameover then
      sprite.Force().Draw(gameoverTexture.Force(), Vector2(50.f,400.f - float32 !gameoverWait / 40.f), Color(255, 255, 255, 128))
      if !gameoverWait <= 12000. then 
        bgm.Force().Stop() 
        gameSe.[3] |> soundPlay 
        gameoverWait := !gameoverWait + fps * 3.
      else
        ["つづける:Enter",10.f;"やめる:Esc",50.f] |> List.iter (fun (msg,y) -> drawText msg <| Vector2(45.f, 160.f + y) <| Color.Gold)
    else
      gameoverWait := 0.
      if ps.current.hidden |> not then
        ps.current.position ||> fun x y ->
        (ps.current.pattern) |> PuyoPuyoLibrary.PuyoPuyo.convert |> Array2D.iteri 
          (fun i j c -> if y + j > 0 then drawPuyo(c, x , y, (i, j)) location (uncoupling c 1) true)
        sprite.Force().Draw(backgroundTexture.Force(), Vector2(0.f,0.f),Nullable(Rectangle(0,0,256,32)), Color.White)) <| Vector2(32.f, 0.f) 

  let drawErase = 
    let animeWait = ref 0.
    (fun f (location:Vector2) () ->
      if ps.etarget <> List.empty then
        animeWait := !animeWait + fps
        let draw union = List.iter (fun (x,y,c) -> if y > 0 then drawPuyo(c, x, y, (0, 0)) location (Rectangle(ps.pw * union ,ps.ph * (int c - 1), ps.pw, ps.ph)) false) ps.etarget
        if !animeWait < 780. then draw 16 elif !animeWait < 1560. then draw 17
        else animeWait := 0.; f()) <| (fun () -> ps <- { ps with etarget = List.empty }) <| Vector2(32.f, 0.f)

  let drawAnimation (texture:Lazy<Texture2D>) width hight max vector = 
    let animeWait, counter = ref 0., ref 0
    (fun (location:Vector2) () -> 
      animeWait := !animeWait + fps
      if !animeWait > 780. - (30. * float (PuyoPuyo.getLevel ps.erased + 1)) then 
        animeWait := 0.; 
        if not ps.pause then incr counter  
      sprite.Force().Draw(texture.Force(), location, Nullable (Rectangle(width * !counter,0,width,hight)),  Color.White)
      if !counter = max then   counter := 0) <| vector
  let drawCarbancle = drawAnimation <| carbancleTexture <| 25 <| 25 <| 37 <| Vector2(260.f,388.f)
  let drawSukesoudara = drawAnimation <| suketoudaraTexture <| 72 <| 54 <| 10 <| Vector2(236.f,228.f)

  let drawNext () = 
    let drawPuyo' pattern location adjustx =  pattern |> PuyoPuyoLibrary.PuyoPuyo.convert |> Array2D.iteri
                                               (fun i j c -> drawPuyo (c, 1, j, (0, 0)) location (uncoupling c adjustx) false)
    Vector2(224.f, 96.f) |> fun location -> [ps.next.[0].pattern, location, 1; ps.next.[1].pattern, (Vector2(location.X + 32.f, location.Y + 32.f)), 2] 
      |> List.iter (fun (p,v,a) ->  drawPuyo' p v a) 

  let drawAllClear () = if ps.allclear then sprite.Force().Draw(allclearTexture.Force(), Vector2(45.f,50.f),  Color.White)
  let drawPause () = if ps.pause then drawText "PAUSE" <| Vector2(96.f,150.f) <| Color.Gold 
  let drawRensa = 
    let rensaWait, rensaCount = ref 0., ref 0 
    (fun () -> if ps.pause |> not then rensaWait := !rensaWait + fps * 1.5
               if ps.rensa > 0 && !rensaWait < 25. * fps then
                 (3,7) ||> fun x y -> drawText (string ps.rensa + "れんさ") <| Vector2(32.f * float32 x, 32.f * float32 y - float32(!rensaWait / 40.)) <| Color.HotPink 
               if ps.rensa <> !rensaCount && !rensaWait > 50. * fps then
                 rensaCount := ps.rensa; rensaWait := 0. )
  let save () = 
    let save maxLevel highScore = 
      let result = StorageDevice.BeginShowSelector(PlayerIndex.One, null, null)
      let device = StorageDevice.EndShowSelector(result)
      saveStorage device maxLevel highScore 
      ps <- { ps with maxLevel = maxLevel; highScore = highScore }
    let maxlevelAndHighScore () = if PuyoPuyo.getLevel ps.erased > ps.maxLevel then PuyoPuyo.getLevel ps.erased else ps.maxLevel 
                                , if ps.totalScore > ps.highScore then ps.totalScore else ps.highScore
    if PuyoPuyo.getLevel ps.erased > ps.maxLevel || ps.totalScore > ps.highScore then
      maxlevelAndHighScore () ||> fun maxLevel highScore -> save maxLevel highScore

  let update = 
    let updateWait, updateTimelag = ref 0., ref (72. * fps + (15. * float (PuyoPuyo.getLevel ps.erased)))
    let chain = 
      (fun cont ->
        if ps.etarget = List.empty && PuyoPuyo.fall ps > 0 then
          ps <- { ps with falling = true }
          updateTimelag := 24. * fps + (15. * float (PuyoPuyo.getLevel ps.erased + 1))
        else
          PuyoPuyo.erase ps |> function
          | etarget,union,colors,newps when union <= 0 -> 
            ps <- { newps with rensa = 0; union = union; colors = colors } 
            PuyoPuyo.getAllclearScore ps |> fun z -> 
            if z > decimal 0 then
              gameSe.[4] |> soundPlay
              ps <- { ps with totalScore = (z |> fun x -> x + ps.totalScore); allclear = true }
            ps <- { ps with falling = false }
            cont ()
            if PuyoPuyo.detectCollision ps.current ps.field then
              ps <- { ps with gameover = true } ; save()
            updateTimelag := 72. * fps + (15. * float (PuyoPuyo.getLevel ps.erased))
          | etarget,union,colors,newps -> 
            List.iter (fun (x,y,_) -> ps.field.[x,y] <- PuyoColors.n) etarget
            async { newps.rensa |> (fun x -> if x > 19 then chainVoice.[19] else chainVoice.[x]) |> soundPlay } |> Async.Start
            async { newps.rensa |> (fun x -> if x > 19 then chainSe.[19] else chainSe.[x]) |> soundPlay } |> Async.Start
            ps <- { newps with rensa = newps.rensa + 1; union = union; colors = colors; erased = newps.erased + union; cheat = false }
            let scoreBase,magnifyingPower = PuyoPuyo.getScore ps
            ps <- { ps with totalScore = decimal (scoreBase * magnifyingPower) + ps.totalScore; 
                            allclear = false; etarget = etarget; scoreBase = scoreBase; magnifyingPower = magnifyingPower })
    fun () -> updateWait := !updateWait + fps + (15. * float (PuyoPuyo.getLevel ps.erased))
              if !updateWait > !updateTimelag then
                updateWait := 0.
                let puyo = PuyoPuyo.descend ps
                if PuyoPuyo.detectCollision puyo ps.field |> not then
                  ps <- { ps with current = puyo }
                if ps.falling && ps.etarget = List.empty then
                    chain (fun () -> ps <- PuyoPuyo.nextPuyo ps)
              if !updateWait < 36. * fps then ps <- { ps with current = { ps.current with upside = true } } 
              else ps <- { ps with current = { ps.current with upside = false } }
  do
    this.Window.Title <- gametitle
    (320,448) ||> fun x y -> gmanager.PreferredBackBufferWidth <- x ; gmanager.PreferredBackBufferHeight <- y
    this.TargetElapsedTime <- TimeSpan.FromSeconds(1.0 / fps)

  override thi.Initialize() = base.Initialize() |> fun _ ->
    let result = StorageDevice.BeginShowSelector(PlayerIndex.One, null, null)
    let device = StorageDevice.EndShowSelector(result)
    loadStorage device ||> fun maxLevel highScore -> ps <- { ps with maxLevel = maxLevel; highScore = highScore }
    bgm |> soundPlay

  override this.Update(gameTime) = 
    checkPauseKey ()
    if ps.pause |>  not then
      base.Update gameTime
      [operateKeys;update] |> List.iter (fun f -> f ())
      if not ps.gameover then
        if PuyoPuyo.detectCollision (PuyoPuyo.descend ps) ps.field then
          if not ps.falling then 
            gameSe.[2] |> soundPlay; ps <- PuyoPuyo.fixed' ps
        elif ps.current.pattern = PuyoPuyo.none && not ps.falling then 
             ps <- PuyoPuyo.nextPuyo ps

  override this.Draw(gameTime) = base.Draw gameTime |> fun _ ->
    gmanager.GraphicsDevice.Clear(Color.Black)
    sprite.Force().Begin ()
    sprite.Force().Draw(backgroundTexture.Force(), Vector2.Zero, Color.White)
    [drawAllClear;drawField;drawNext;drawSukesoudara;drawCarbancle;drawErase;drawRensa;drawLiteral;drawScoreAndLevel;drawPause] |> List.iter (fun f -> f())
    sprite.Force().End ()

  override this.EndRun () = base.EndRun(); save()

module Program =
  [<EntryPoint>]
  let main (args : string[]) = use game = new PuyoGame() in game.Run(); 0

画像データ、音データに関しましては、動画をごらん頂けばわかるとおり配布することはできません。お察しください。
コードを読み理解するとができれば、どんな画像や音データを用意すれば適切に動作させられるかわかるはず。
なお、コードは読めなくてもまったく問題ありません。それでは、CLR/H勉強会でお会いしましょう。