Search code examples
f#functional-programmingroguelike

Very simple RogueLike in F#, making it more "functional"


I have some existing C# code for a very, very simple RogueLike engine. It is deliberately naive in that I was trying to do the minimum amount as simply as possible. All it does is move an @ symbol around a hardcoded map using the arrow keys and System.Console:

//define the map
var map = new List<string>{
  "                                        ",
  "                                        ",
  "                                        ",
  "                                        ",
  "    ###############################     ",
  "    #                             #     ",
  "    #         ######              #     ",
  "    #         #    #              #     ",
  "    #### #### #    #              #     ",
  "       # #  # #    #              #     ",
  "       # #  # #    #              #     ",
  "    #### #### ######              #     ",
  "    #              =              #     ",
  "    #              =              #     ",
  "    ###############################     ",
  "                                        ",
  "                                        ",
  "                                        ",
  "                                        ",
  "                                        "
};

//set initial player position on the map
var playerX = 8;
var playerY = 6;

//clear the console
Console.Clear();

//send each row of the map to the Console
map.ForEach( Console.WriteLine );

//create an empty ConsoleKeyInfo for storing the last key pressed
var keyInfo = new ConsoleKeyInfo( );

//keep processing key presses until the player wants to quit
while ( keyInfo.Key != ConsoleKey.Q ) {
  //store the player's current location
  var oldX = playerX;
  var oldY = playerY;

  //change the player's location if they pressed an arrow key
  switch ( keyInfo.Key ) {
    case ConsoleKey.UpArrow:
      playerY--;
      break;
    case ConsoleKey.DownArrow:
      playerY++;
      break;
    case ConsoleKey.LeftArrow:
      playerX--;
      break;
    case ConsoleKey.RightArrow:
      playerX++;
      break;
  }

  //check if the square that the player is trying to move to is empty
  if( map[ playerY ][ playerX ] == ' ' ) {
    //ok it was empty, clear the square they were standing on before
    Console.SetCursorPosition( oldX, oldY );
    Console.Write( ' ' );
    //now draw them at the new square
    Console.SetCursorPosition( playerX, playerY );
    Console.Write( '@' );
  } else {
    //they can't move there, change their location back to the old location
    playerX = oldX;
    playerY = oldY;
  }

  //wait for them to press a key and store it in keyInfo
  keyInfo = Console.ReadKey( true );
}

I was playing around with doing it in F#, initially I was trying to write it using functional concepts, but turned out I was a bit over my head, so I did pretty much a straight port - it's not really an F# program (though it compiles and runs) it's a procedural program written in F# syntax:

open System

//define the map
let map = [ "                                        ";
            "                                        ";
            "                                        ";
            "                                        ";
            "    ###############################     ";
            "    #                             #     ";
            "    #         ######              #     ";
            "    #         #    #              #     ";
            "    #### #### #    #              #     ";
            "       # #  # #    #              #     ";
            "       # #  # #    #              #     ";
            "    #### #### ######              #     ";
            "    #              =              #     ";
            "    #              =              #     ";
            "    ###############################     ";
            "                                        ";
            "                                        ";
            "                                        ";
            "                                        ";
            "                                        " ]

//set initial player position on the map
let mutable playerX = 8
let mutable playerY = 6

//clear the console
Console.Clear()

//send each row of the map to the Console
map |> Seq.iter (printfn "%s")

//create an empty ConsoleKeyInfo for storing the last key pressed
let mutable keyInfo = ConsoleKeyInfo()

//keep processing key presses until the player wants to quit
while not ( keyInfo.Key = ConsoleKey.Q ) do
    //store the player's current location
    let mutable oldX = playerX
    let mutable oldY = playerY

    //change the player's location if they pressed an arrow key
    if keyInfo.Key = ConsoleKey.UpArrow then
        playerY <- playerY - 1
    else if keyInfo.Key = ConsoleKey.DownArrow then
        playerY <- playerY + 1
    else if keyInfo.Key = ConsoleKey.LeftArrow then
        playerX <- playerX - 1
    else if keyInfo.Key = ConsoleKey.RightArrow then
        playerX <- playerX + 1

    //check if the square that the player is trying to move to is empty
    if map.Item( playerY ).Chars( playerX ) = ' ' then
        //ok it was empty, clear the square they were standing on
        Console.SetCursorPosition( oldX, oldY )
        Console.Write( ' ' )
        //now draw them at the new square 
        Console.SetCursorPosition( playerX, playerY )
        Console.Write( '@' )
    else
        //they can't move there, change their location back to the old location
        playerX <- oldX
        playerY <- oldY

    //wait for them to press a key and store it in keyInfo
    keyInfo <- Console.ReadKey( true )

So my question is, what do I need to learn in order to rewrite this more functionally, can you give me some hints, a vague overview, that kind of thing.

I'd prefer a shove in the right direction rather than just seeing some code, but if that's the easiest way for you to explain it to me then fine, but in that case can you please also explain the "why" rather the "how" of it?


Solution

  • Game programming in general will test your ability to manage complexity. I find that functional programming encourages you to break problems your solving into smaller pieces.

    The first thing you want to do is turn your script into a bunch of functions by separating all the different concerns. I know it sounds silly but the very act of doing this will make the code more functional (pun intended.) Your main concern is going to be state management. I used a record to manage the position state and a tuple to manage the running state. As your code gets more advanced you will need objects to manage state cleanly.

    Try adding more to this game and keep breaking the functions apart as they grow. Eventually you will need objects to manage all the functions.

    On a game programming note don't change state to something else and then change it back if it fails some test. You want minimal state change. So for instance below I calculate the newPosition and then only change the playerPosition if this future position passes.

    open System
    
    // use a third party vector class for 2D and 3D positions
    // or write your own for pratice
    type Pos = {x: int; y: int} 
        with
        static member (+) (a, b) =
            {x = a.x + b.x; y = a.y + b.y}
    
    let drawBoard map =
        //clear the console
        Console.Clear()
        //send each row of the map to the Console
        map |> List.iter (printfn "%s")
    
    let movePlayer (keyInfo : ConsoleKeyInfo) =
        match keyInfo.Key with
        | ConsoleKey.UpArrow -> {x = 0; y = -1}
        | ConsoleKey.DownArrow -> {x = 0; y = 1}
        | ConsoleKey.LeftArrow -> {x = -1; y = 0}
        | ConsoleKey.RightArrow  -> {x = 1; y = 0}
        | _ -> {x = 0; y = 0}
    
    let validPosition (map:string list) position =
        map.Item(position.y).Chars(position.x) = ' '
    
    //clear the square player was standing on
    let clearPlayer position =
        Console.SetCursorPosition(position.x, position.y)
        Console.Write( ' ' )
    
    //draw the square player is standing on
    let drawPlayer position =
        Console.SetCursorPosition(position.x, position.y)
        Console.Write( '@' )
    
    let takeTurn map playerPosition =
        let keyInfo = Console.ReadKey true
        // check to see if player wants to keep playing
        let keepPlaying = keyInfo.Key <> ConsoleKey.Q
        // get player movement from user input
        let movement = movePlayer keyInfo
        // calculate the players new position
        let newPosition = playerPosition + movement
        // check for valid move
        let validMove = newPosition |> validPosition map
        // update drawing if move was valid
        if validMove then
            clearPlayer playerPosition
            drawPlayer newPosition
        // return state
        if validMove then
            keepPlaying, newPosition
        else
            keepPlaying, playerPosition
    
    // main game loop
    let rec gameRun map playerPosition =
        let keepPlaying, newPosition = playerPosition |> takeTurn map 
        if keepPlaying then
            gameRun map newPosition
    
    // setup game
    let startGame map playerPosition =
        drawBoard map
        drawPlayer playerPosition
        gameRun map playerPosition
    
    
    //define the map
    let map = [ "                                        ";
                "                                        ";
                "                                        ";
                "                                        ";
                "    ###############################     ";
                "    #                             #     ";
                "    #         ######              #     ";
                "    #         #    #              #     ";
                "    #### #### #    #              #     ";
                "       # #  # #    #              #     ";
                "       # #  # #    #              #     ";
                "    #### #### ######              #     ";
                "    #              =              #     ";
                "    #              =              #     ";
                "    ###############################     ";
                "                                        ";
                "                                        ";
                "                                        ";
                "                                        ";
                "                                        " ]
    
    //initial player position on the map
    let playerPosition = {x = 8; y = 6}
    
    startGame map playerPosition