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
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.
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.
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).
Let's draw (missing the process of dying).
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.
Coins Set by position and speed of rotation
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
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.
The most interesting character. To set it, you need a lot of parameters -
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
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
To check the intersection with RedSquare, we use the same method as when implementing YellowSquare.
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).
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
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
To intercept the key, as it turned out, nothing complicated
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
Since this function is implemented after all classes, you need to add its delegate to the form
(dd.Invoke) executes a function with the specified parameters.
Of course, this implementation is not flexible or optimal. The code and the levels themselves are in a state of refinement. I will be glad to hear comments and suggestions.
UPD. Code + exe + 2 level
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
} |> Seq.head
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()
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 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
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, 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=.../*куча параметров*/)
form.Paint.Add(fun arg ->
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))
|> 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 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
- Card sizes
- Map
- BlueCircle Number
- Parameters of each of them
- Number YellowCircle
- Parameters of each of them
- 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 =
...
letmutable (dd:DelegateLoad) = null
...
member x.LoadNext() =
currLevel <- currLevel + 1let pathToLevel = pathToFolder+"\\"+"L"+currLevel.ToString()+".txt"if (File.Exists(pathToLevel) = false) then
complete <- 1else
x.Load(dd.Invoke(pathToLevel))
x.Invalidate()
(dd.Invoke) executes a 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 refinement. I will be glad to hear comments and suggestions.
UPD. Code + exe + 2 level