Simple RPG
Saturday, February 12, 2011
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
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.