Saturday, February 12, 2011

Simple RPG

There was a post a few days ago that discussed "fluent interfaces" using a simple role-playing game as an example. Since Factor, as a concatenative language, can be quite "fluent", I decided to port the original C# version into Factor to see how it looks.

The basic "simple RPG" game starts with the creation of a hero, who encounters and does battle with various randomly generated enemies. It is text-based and looks something like this:

= Starting Battle =
Name: Valient
Class: fighter
HP: 20/20
Age: 22
Str 18 / Agi 14 / Int 12
Gold: 0

vs.

Name: Destroicous
Class: fighter
HP: 8/8
Age: 107
Str 7 / Agi 6 / Int 18
Gold: 42

An enemy approaches> 


Valient (20/20) / Destroicous (8/8)
Valient poisons Destroicous for 6 damage!
Destroicous incinerates Valient for 3 damage!

Some vocabularies we will be using:

USING: accessors combinators formatting io kernel locals math
math.ranges random sequences ;

Object Creation

The original article focuses mainly on object creation, which I'll only mention in passing. The "fluent" version in C# looks like this:

characterBuilder.Create("King")
   .As(ClassType.Fighter)
   .WithAge(49)
   .HP(50)
   .Strength(17)
   .Agility(12)
   .Intelligence(15)
   .Gold(9999999);

Translating that directly to Factor (using slot accessors to construct objects), would look something like this :

character new
    "King" >>name
    "fighter" >>class
    49 >>age
    50 >>hp
    17 >>strength
    12 >>agility
    15 >>intelligence
    9999999 >>gold

The Character

We define a character type with some basic fields (using short names that are common among RPG programmers) to represent either our hero, or his enemies:

TUPLE: character name class age str agi int gold hp max-hp ;

We will list several possible character classes (although the main logic will not take these into account):

CONSTANT: classes {
    "fighter"
    "mage"
    "cleric"
    "rogue"
}

A word to check if a character is alive:

: alive? ( character -- ? ) hp>> 0 > ;

We can print out our character's full stats:

: full-stats ( character -- )
    {
        [ name>> "Name: %s\n" printf ]
        [ class>> "Class: %s\n" printf ]
        [ [ hp>> ] [ max-hp>> ] bi "HP: %d/%d\n" printf ]
        [ age>> "Age: %d\n" printf ]
        [
            [ str>> ] [ agi>> ] [ int>> ] tri
            "Str %d / Agi %d / Int %d\n" printf
        ]
        [ gold>> "Gold: %d\n" printf ]
    } cleave ;

Or print just a "quick" version:

: quick-stats ( character -- )
    [ name>> ] [ hp>> ] [ max-hp>> ] tri "%s (%d/%d)" printf ;

The Battle

Note: Our battle logic is implemented with locals to try and match the C# version closely.

We support some random attack types:

CONSTANT: attack-verbs {
    "slashes"
    "stabs"
    "smashes"
    "impales"
    "poisons"
    "shoots"
    "incinerates"
    "destroys"
}

The attack logic is very simple - a random amount of damage using a random attack type:

:: attack ( attacker defender -- )
    10 random :> damage
    attacker name>>
    attack-verbs random
    defender [ damage - ] change-hp name>>
    damage
    "%s %s %s for %d damage!\n" printf ;

The main battle logic starts a battle, loops performing a "fight to the death", and then declares our hero as victor or victim:

:: battle ( hero enemy -- )
    "= Starting Battle =" print
    hero full-stats nl
    "vs." print nl
    enemy full-stats nl
    "An enemy approaches> " write read1 drop nl nl

    [ hero alive? enemy alive? and ] [
        hero quick-stats " / " write enemy quick-stats nl
        hero enemy attack
        enemy hero attack
        hero alive? [ "> " write read1 drop ] when nl
    ] while

    hero alive? [
        "Our hero survives to fight another battle! " write
        enemy gold>> "Won %d gold!\n" printf
        hero [ enemy gold>> + ] change-gold drop
    ] [
        hero gold>> "Our hero has fallen with %d gold! " printf
        "The world is covered in darkness once again." print
    ] if nl ;

The Game

We create "Valient", our hero:

: <hero> ( -- character )
    character new
        "Valient" >>name
        "fighter" >>class
        22 >>age
        20 [ >>hp ] [ >>max-hp ] bi
        18 >>str
        14 >>agi
        12 >>int
        0 >>gold ;

Some logic to create random enemy names:

CONSTANT: first-names {
    "Destro"
    "Victo"
    "Mozri"
    "Fang"
    "Ovi"
    "Hell"
    "Syth"
    "End"
}

CONSTANT: last-names {
    "math"
    "rin"
    "sith"
    "icous"
    "ravage"
    "wrath"
    "ryn"
    "less"
}

: random-name ( -- str )
    first-names last-names [ random ] bi@ append ;

And finally, create a random enemy to fight:

: <enemy> ( -- character )
    character new
        random-name >>name
        classes random >>class
        12 200 [a,b] random >>age
        5 12 [a,b] random [ >>hp ] [ >>max-hp ] bi
        21 [1,b) random >>str
        21 [1,b) random >>agi
        21 [1,b) random >>int
        50 random >>gold ;

Using these words, and the battle logic created earlier, we can run the entire game:

: run-battle ( -- )
    <hero> [ dup alive? ] [ dup <enemy> battle ] while drop ;

The code for this is on my Github.

1 comment:

tgkuo said...

good teaching example, it maybe in the Chapter 1 introduction material for a Factor book, thanks.