F # The most difficult game in the world

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

Quickly, but

Main objects

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

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

Redsquare

The simplest object for working with which you need to know only its current parameters (position, size) and condition (alive or dying, since it will die gradually).
``````type RedSquare(xx:int, yy:int, ww:int, hh:int, speed:int) =
...
member rs.X withget() = int xCoord andset(v) = (xCoord <- v)
member rs.Y withget() = int yCoord andset(v) = (yCoord <- v)
member rs.W withget() = width andset(v) = (width <- v)
member rs.H withget() = height andset(v) = (height <- v)
member rs.Got withget() = gather // сколько монеток съедено
member rs.isDying withget() = (dying>0)
member rs.Speed = speed
``````

Let's draw (missing the process of dying).
``````    interface IPaintObject withmember 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 implementing Recalc. The difficulty is not to go beyond the boundaries of the map. But more on that later, since we still do not know how to set the level.

Yellowcircle

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

There is nothing interesting in the implementation of the class, only you need to check if it intersects 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 domatch obj with
| :? RedSquare as p ->
yield p
| _ -> yield! Seq.empty
``````

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

Next is the standard task of intersecting a circle and a square. If it crosses, 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()
``````

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 a closed set of segments along which it will move. Segments are specified as vectors (dx, dy). That is, 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, it will return to the first.
In this implementation, it is not possible to move an object in a circle (unless you can make it a many, many, polygon and move along small vectors).
Some basic class properties
``````    member bc.Stablewithget() = (bc.TotalDist < 1e-8) // стабильный или динамический
member bc.Speed withget() = 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
``````

We realize Recalc.
It’s good that there is the possibility of taking modulo fractional numbers. So, as the path of the circle 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 as a 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 (in the presence of all collected coins, of course).

The form

Yes, all this is good, but it’s time to decide on what and how to draw it all.
Define the SmoothForm class inherited from Form and add some of our methods
``````type SmoothForm(dx:int, dy:int, _path:string) as x =
inherit Form()
do x.DoubleBuffered <- true
...
let mutable Map = null
Map <- _map
po.Clear()
for o in obj do
need <- _need
x.Init()
``````

x.Load loads the level, according to the map, an array of objects and the number of coins that must be collected in order 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 interception of keystrokes

``````let form = new SmoothForm(Text="F# The world hardest game",  Visible=true, TopMost=true,Width=.../*куча параметров*/)
let g = arg.Graphicsfor i=0 to form.rows-1dofor j=0 to form.cols-1do
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
``````form.KeyDown
// из всех нажатий оставим нажатия стрелочками
|> Event.filter(fun args -> (args.KeyValue >= 37) && (args.KeyValue <= 40))
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 a level from files. To do this, we write a function that takes the file path as a parameter and returns level parameters. The file will go

1. Card sizes
2. Map
3. BlueCircle Number
4. Parameters of each of them
5. Number YellowCircle
6. Parameters of each of them
7. RedSquare coordinates, dimensions and speed

``````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 = delegateof (string) -> (int[][]*ResizeArray<IPaintObject>*int)
type SmoothForm(dx:int, dy:int, _path:string) as x =
...
...
currLevel <- currLevel + 1let pathToLevel = pathToFolder+"\\"+"L"+currLevel.ToString()+".txt"if (File.Exists(pathToLevel) = false) then
complete <- 1else