F#世界で最も難しいゲーム

関数プログラミング、特にF#の可能性に触発され、ほんの数十行を作成できる例を見て 、私は最も複雑なフラッシュゲームの単純なバージョンを実装することにしました。



すぐに、しかし





主なオブジェクト





最初に、作業する必要があるオブジェクトのタイプを決定します。 明らかに、私たちは赤の広場、黄色のコイン、独立した青い殺人者の形で自分自身になります。 これらのクラスはすべて、インターフェースを実装します。

type IPaintObject = abstract Paint : Graphics -> unit abstract Recalc : float -> unit
      
      





ペイントはフォーム上に描画され、Recalc(時間)はオブジェクトがどこにあるかを計算します。

すべてのオブジェクトは1つの配列になります。

 let po = new ResizeArray<IPaintObject>()
      
      







赤の広場


現在のパラメーター(位置、サイズ)と状態(徐々に死ぬため、生きているか死にかけている)のみを知る必要がある、作業用の最も単純なオブジェクト。

 type RedSquare(xx:int, yy:int, ww:int, hh:int, speed:int) = ... member rs.X with get() = int xCoord and set(v) = (xCoord <- v) member rs.Y with get() = int yCoord and set(v) = (yCoord <- v) member rs.W with get() = width and set(v) = (width <- v) member rs.H with get() = height and set(v) = (height <- v) member rs.Got with get() = gather //    member rs.isDying with get() = (dying>0) member rs.Speed = speed
      
      







描いてみましょう(死ぬプロセスを逃します)。

  interface IPaintObject with member obj.Paint(g) = let rect = match (dying) with | 0 -> Rectangle(x=int xCoord-width/2, y=int yCoord-height/2, width=width, height=height) ... g.FillRectangle(Brushes.Red, rect) g.DrawRectangle(new Pen(Color.Black, float32 2), rect)
      
      







難しいのは、Recalcの実装です。 困難は、マップの境界を越えないことです。 しかし、レベルの設定方法がまだわからないため、後で詳しく説明します。



イエローサークル


硬貨 位置と回転速度で設定

 type YellowCircle(xx:int, yy:int, rr:int, tr:float) = ...
      
      







クラスの実装に興味深いものはありません。RedSquareと交差するかどうかを確認する必要があるだけです。 これは、Recalcメソッドで実行できます。

まず、配列から赤い正方形を描きます

  let rs = seq { for obj in po do match obj with | :? RedSquare as p -> yield p | _ -> yield! Seq.empty } |> Seq.head
      
      





最適な方法ではなく、AFの可能性が示されています。 タイプがRedSquareの場合はオブジェクトが追加されるセットが作成され、それ以外の場合は何も作成されません。 したがって、RedSquareは1つだけなので、Seq.headを取得します



次に、円と正方形を交差させる標準的な問題があります。 それが交差する場合、コインを殺し、私たちの資産に1ポイントを追加します。

  if (isIntersects xx yy rr (rs.X-rs.W/2) (rs.Y-rs.H/2) (rs.W) (rs.H)) then yc.Take() rs.Add()
      
      







ブルーサークル


最も興味深いキャラクター。 設定するには、多くのパラメーターが必要です-
 type BlueCircle(xx:int, yy:int, rr:int, speed:int, segments:(int*int)[]) =
      
      





座標、半径、速度、移動するセグメントの閉じたセット。 セグメントはベクトル(dx、dy)として指定されます。 つまり、現在の位置から、円は最初のセグメントに沿って移動し、次に対応する2番目のベクトルになります。 最後のベクトルの後、最初のベクトルに戻ります。

この実装では、オブジェクトを円状に移動することはできません(オブジェクトを多数、多数、多角形にして小さなベクトルに沿って移動できる場合を除く)。

いくつかの基本的なクラスプロパティ

  member bc.Stable with get() = (bc.TotalDist < 1e-8) //    member bc.Speed with get() = float speed member bc.Dists = segments |> Array.map(fun (dx, dy) -> Math.Sqrt(float(dx*dx+dy*dy))) //   member bc.TotalDist = bc.Dists |> Array.sum member bc.TotalTime = bc.TotalDist/bc.Speed
      
      







Recalcを実現します。

モジュロ小数を取る可能性があるのは良いことです。 したがって、円のパスは周期的であり、その通過時間を知っているので、現在の位置を決定できます

  member bc.Recalc(tt) = //   -  ,  if (bc.Stable=false) then let mutable t1 = tt%bc.TotalTime let mutable ind = 0 X <- xx Y <- yy //     -  ,     while (ind<len-1 && t1*bc.Speed>=bc.Dists.[ind]) do X <- X + (fst segments.[ind]) Y <- Y + (snd segments.[ind]) t1 <- t1-bc.Dists.[ind]/bc.Speed ind <- ind+1 //    let (dx, dy) = (((float (fst segments.[ind]))/(bc.Dists.[ind])), ((float (snd segments.[ind]))/(bc.Dists.[ind]))) X <- X + int (dx*t1*bc.Speed) Y <- Y + int (dy*t1*bc.Speed)
      
      







RedSquareとの交差を確認するには、YellowSquareを実装するときと同じ方法を使用します。



地図


自然な解決策は、マップをマトリックスとして設定することでした。 以下の表記を紹介します

-1-禁止ゾーン

0-フリーセル

> 0-チェックポイント(緑の領域)。 保存できます。 最大数はラウンドの終わりを示します(もちろん、収集されたすべてのコインが存在する場合)。





はい、これはすべて良いことですが、すべてをどのように、どのように描くかを決定する時です。

Formから継承したSmoothFormクラスを定義し、いくつかのメソッドを追加します

 type SmoothForm(dx:int, dy:int, _path:string) as x = inherit Form() do x.DoubleBuffered <- true ... let mutable Map = null member x.Load(_map:int[][], obj, _need) = Map <- _map po.Clear() for o in obj do po.Add o need <- _need x.Init()
      
      







x.Loadは、マップ、オブジェクトの配列、およびレベルを完了するために収集する必要があるコインの数に従って、レベルをロードします。

x.Initは、主に各緑の領域の保存ポイントの座標の計算に関係しています。



実際には、Paintメソッドとキーストロークのインターセプトを定義するために残っています



 let form = new SmoothForm(Text="F# The world hardest game", Visible=true, TopMost=true,Width=.../* */) form.Paint.Add(fun arg -> let g = arg.Graphics for i=0 to form.rows-1 do for j=0 to form.cols-1 do match (form.map.[i].[j], (i+j)%2) with //   | (-1, _) -> g.FillRectangle(Brushes.DarkViolet, j*form.DX, i*form.DY, form.DX, form.DY) //   | ( 0, 0) -> g.FillRectangle(Brushes.White, j*form.DX, i*form.DY, form.DX, form.DY) //   | ( 0, 1) -> g.FillRectangle(Brushes.LightGray, j*form.DX, i*form.DY, form.DX, form.DY) //   | ( p, _) when p>0 -> g.FillRectangle(Brushes.LightGreen, j*form.DX, i*form.DY+1, form.DX, form.DY) //  if (i>0 && (form.map.[i].[j]>=0 && form.map.[i-1].[j]<0 || form.map.[i].[j]<0 && form.map.[i-1].[j]>=0)) then g.DrawLine(new Pen(Color.Black, float32 2), j*form.DX, i*form.DY, (j+1)*form.DX, i*form.DY) //  if (j>0 && (form.map.[i].[j]>=0 && form.map.[i].[j-1]<0 || form.map.[i].[j]<0 && form.map.[i].[j-1]>=0)) then g.DrawLine(new Pen(Color.Black, float32 2), j*form.DX, i*form.DY, j*form.DX, (i+1)*form.DY) for obj in po do //     obj.Recalc((DateTime.Now-SS).TotalSeconds) obj.Paint(g) async { do! Async.Sleep(10) //  10 form.Invalidate() } |> Async.Start )
      
      







判明したように、キーを傍受するために、複雑なものはありません

 form.KeyDown //       |> Event.filter(fun args -> (args.KeyValue >= 37) && (args.KeyValue <= 40)) |> Event.add (fun args -> match (args.KeyCode) with | Keys.Down -> form.Down <- 1 | Keys.Left -> form.Left <- 1 | Keys.Right -> form.Right <- 1 | Keys.Up -> form.Up <- 1 )
      
      





同様にform.KeyUpについて







のようなもの...



ファイルからレベルをロードする方法を学ぶことは残っています。 これを行うには、ファイルパスをパラメーターとして取り、レベルパラメーターを返す関数を作成します。 ファイルが行きます

  1. カードサイズ
  2. 地図
  3. BlueCircle番号
  4. それらのそれぞれのパラメーター
  5. ナンバーイエローサークル
  6. それらのそれぞれのパラメーター
  7. RedSquareの座標、寸法、速度




 let LoadLevel _path = let pp = new ResizeArray<IPaintObject>() let data = File.ReadAllLines(_path) |> Array.toSeq; let L1 = data |> Seq.skip 1 |> Seq.take n |> Seq.toArray |> Array.map(fun x -> x.Split([|' '|]) |> Array.filter(fun x -> Int32.TryParse(x, ref tmp)) |> Array.map(fun x -> Int32.Parse(x))) ...
      
      







この関数はすべてのクラスの後に実装されるため、フォームにデリゲートを追加する必要があります



 type DelegateLoad = delegate of (string) -> (int[][]*ResizeArray<IPaintObject>*int) type SmoothForm(dx:int, dy:int, _path:string) as x = ... let mutable (dd:DelegateLoad) = null ... member x.LoadNext() = currLevel <- currLevel + 1 let pathToLevel = pathToFolder+"\\"+"L"+currLevel.ToString()+".txt" if (File.Exists(pathToLevel) = false) then complete <- 1 else x.Load(dd.Invoke(pathToLevel)) x.Invalidate()
      
      







(dd.Invoke)は、指定されたパラメーターで関数を実行します。



おわりに


もちろん、この実装は柔軟でも最適でもありません。 コードレベル自体は洗練された状態にあります。 私はコメントや提案を聞いてうれしいです。



UPD。 コード+ exe + 2レベル



All Articles