Building Roguelike in F#

Silvrback blog image

I know many people who started programming because they wanted to write a game of their own. I myself had never done game programming but after I ran into articles about programming a roguelike in Haskell I decided to give it a try using F#.

First I wanted to get hero to move around on map.

These are the types I came up with:

namespace SharpRogue
module Types =
    type Coordinate = { x:int; y:int; }

    type Hero = {
        currentPosition : Coordinate;
        oldPosition : Coordinate;
    }

    type MapTile = {
        coordinate : Coordinate;
        tile : char;
    }

    type World = {
        tiles : MapTile list
        hero : Hero
    }

    type Input = 
        Up 
        | Down
        | Left
        | Right
        | Open
        | Exit

Game World consists of MapTiles and Hero. MapTile has a coordinate in the world and a char that symbolises content of the tile, for example symbol for wall is '#'. Hero has data of it's location(currentPosition) and location on previous turn(oldPosition). Input is a discriminated union of the possible inputs from the player.

// Graphics.fs
let hideCursor() = System.Console.SetCursorPosition(0,0)

let drawHero (hero:Hero, world:MapTile list) = 
    System.Console.
        SetCursorPosition(hero.currentPosition.x, hero.currentPosition.y)
    System.Console.Write '@'
    let found = List.find (Utils.findTile hero.oldPosition) world
    System.Console.
        SetCursorPosition(hero.oldPosition.x, hero.oldPosition.y)
    found.tile |> System.Console.Write
    hideCursor()

let drawTile (tile:MapTile) = 
    System.Console.
        SetCursorPosition(tile.coordinate.x, tile.coordinate.y)
    System.Console.Write tile.tile

let drawWorld world = 
    System.Console.Clear()
    List.map (fun x -> drawTile(x)) world |> ignore

let drawOpenDoor coordinate =
    System.Console.SetCursorPosition(coordinate.x, coordinate.y)
    System.Console.Write '-'
    hideCursor()

Since my roguelike is basically a console app the "graphics" module deals with writing out chars on the correct coordinate in the console. HideCursor-function is used to set cursor on the upper left corner of screen. Otherwise it will stay where a character has been written last. drawHero draws the @-character representing hero in currentPosition of hero record. Original tile from world record will replace @-character in oldPosition of hero. Originally I reprinted whole map after every move, but that caused screen to flicker.

The game logic is located in the Program.fs-file. Starting at the main function:

[<EntryPoint>]
let main argv = 
    generateCoordinates |> drawWorld
    let world = {
        hero = { 
                oldPosition = {x = 1; y = 1;}; 
                currentPosition = {x = 1; y = 1;}; 
        }; 
        tiles = generateCoordinates
    }
    gameLoop world
    0 // return an integer exit code

GenerateCoordinates breaks level map into coordinates and tiles. World initialized with hero starting from the top left corner.

GameLoop is a recursive function that draws hero, gets player input and calls gameLoop again with new state of the world.

let rec gameLoop world =
    drawHero (world.hero, world.tiles)
    let input = getInput()
    match input with
        | Exit -> ()
        | Open -> openDoor world.hero world |> gameLoop
        | _ -> {world with hero = (move input world);} |> gameLoop 

Move returns hero with updated coordinates. Movement is allowed if the wanted location is not a wall(#) or a closed door(+). OpenDoor replaces closed door(+) located next to the hero in the given direction with an open door(-).

let move direction world = 
    let hero = world.hero
    let newCoordinates = getNextCoordinate hero direction
    let found = List.find (Utils.findTile newCoordinates) world.tiles   
    match found.tile with
        | '#' -> hero
        | '+' -> hero         
        | _ -> {hero with currentPosition = newCoordinates; 
                oldPosition = hero.currentPosition} 


let openDoor hero world = 
    let direction = getInput()
    let coordinate = getNextCoordinate hero direction
    let tile = List.find (Utils.findTile coordinate) world.tiles
    if tile.tile = '+' then drawOpenDoor tile.coordinate
    let newTiles = List.map 
        (fun x -> 
            if x.coordinate = coordinate then { x with tile = '-'} 
            else x) world.tiles
    {world with hero = hero; tiles = newTiles}

Here is the amazing result!

Silvrback blog image

Not quite Nethack yet, but we'll see what features future brings. The codes for SharpRogue can be found on GitHub.

Recommended content