Building Roguelike in F#

May 19, 2015

Read time 3 min

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 = 
        | 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) = 
        SetCursorPosition(hero.currentPosition.x, hero.currentPosition.y)
    System.Console.Write '@'
    let found = List.find (Utils.findTile hero.oldPosition) world
        SetCursorPosition(hero.oldPosition.x, hero.oldPosition.y)
    found.tile |> System.Console.Write

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

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

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

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:

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

Sign up for our newsletter

Get the latest from us in tech, business, design – and why not life.