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.
good teaching example, it maybe in the Chapter 1 introduction material for a Factor book, thanks.
ReplyDelete