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
                                } |> 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






    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 =
        ...
        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

    Also popular now: