📜 ⬆️ ⬇️

F # The most difficult game in the world

Inspired by the possibilities of functional programming, in particular F #, and seeing by example that you can create only a few dozen lines, I decided to implement a simple version of the most complex flash game.

It turned out in haste, but


Main objects


')
First we will determine what type of objects we will have to work with. Obviously this will be ourselves in the form of a red square, yellow coins and hated blue killers. All these classes will implement the interface.
type IPaintObject = abstract Paint : Graphics -> unit abstract Recalc : float -> unit 

Paint will draw on the form, and Recalc (time) will calculate where the object will be located at the time point.
All objects will be located in one array.
 let po = new ResizeArray<IPaintObject>() 


RedSquare

The simplest object, to work with which you need to know only its current parameters (position, size) and state (alive or dying, as it will die gradually).
 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 


Let's start drawing (having missed the process of dying).
  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) 


The hard part is to implement Recalc. The difficulty is not to go beyond the boundaries of the map. But more about that later, since we still do not know how to set the level.

Yellowcircle

Coins. Set by position and rotation speed
 type YellowCircle(xx:int, yy:int, rr:int, tr:float) = ... 


There is nothing interesting in the implementation of the class, it is only necessary to check if it does not intersect with RedSquare. This can be done in the Recalc method.
First, draw a red square from the array
  let rs = seq { for obj in po do match obj with | :? RedSquare as p -> yield p | _ -> yield! Seq.empty } |> Seq.head 

Not the optimal method, the capabilities of the OP are shown. A set is created, to which an object is added, if it is of the RedSquare type and nothing — if of any other. So, as RedSquare only one - we take Seq.head

Next comes the standard task of intersecting a circle and a square. If it crosses, we kill the coin and add one point to our asset.
  if (isIntersects xx yy rr (rs.X-rs.W/2) (rs.Y-rs.H/2) (rs.W) (rs.H)) then yc.Take() rs.Add() 


Bluecircle

The most interesting character. To set it, you need a lot of parameters -
 type BlueCircle(xx:int, yy:int, rr:int, speed:int, segments:(int*int)[]) = 

coordinates, radius, speed and closed set of segments along which it will move. The segments are specified as vectors (dx, dy). Ie from the current position, the circle will go along the first segment, then turn to the corresponding second vector and so on. After the last vector will return to the first.
In this implementation, it is not possible to move an object in a circle (unless it is made of a many-many-polygon and move along small vectors).
Some basic class properties
  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 


Implement the function Recalc.
As well, there is the possibility of taking modulo fractional numbers. So, as the circle path is cyclic and knowing the time of its passage, you can determine the current position
  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) 


To check the intersection with RedSquare, we use the same method as when implementing YellowSquare.

Map

The natural solution was to set the map matrix. We introduce the following notation.
-1 - forbidden zone
0 - free cell
> 0 - checkpoints (green areas). They can be saved. The maximum number indicates the end of the round (if all the coins are present, of course).

The form

Yes, all this is good, but it's time to decide on what and how to draw it all.
Define a SmoothForm class, inherited from Form, and add a few of its methods.
 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 loads the level, on the map, an array of objects and the number of coins that must be collected to complete the level.
x.Init is mainly concerned with calculating the coordinates of save points for each green area.

Actually, it remains to define the Paint method and intercept keystrokes

 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 ) 


To intercept the key, as it turned out, nothing complicated is needed
 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 ) 

Similarly for form.KeyUp



Something like ...

It remains to learn how to load the level of the files. To do this, we write a function that takes the path to the file as a parameter and returns the level parameters. The file will go
  1. Map dimensions
  2. Map
  3. BlueCircle number
  4. The parameters of each of them
  5. YellowCircle Amount
  6. The parameters of each of them
  7. Coordinates, size and speed 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))) ... 


Since this function is implemented after all classes, you need to add its delegate to the form

 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) performs the function with the specified parameters.

Conclusion

Of course, this implementation is not flexible or optimal. The code and the levels themselves are in a state of improvement. I will be glad to hear comments and suggestions.

UPD. Code + exe + 2 leveled

Source: https://habr.com/ru/post/117276/


All Articles