Writing a Text Adventure Game in Go - Part 2
Mar 26, 2016 · 8 minute read · Comments<<< Part 1: Location and Movement Part 3: Items and Treasure >>>
Intro (Combat)
Welcome back to part 2 of Writing a Text Adventure in Go. Last time we saw how to create a world and have the player navigate in that world. Additionally we had events fire based on chance and affect the players as he progressed thru the world. Those events were what I am going to call passive events since the player had no way of influencing the outcome of those events. Sometimes in the game it’s nice to allow the user to have some control over the events that happen such as attacking or defending during combat or just plain fleeing. For this installment we will enhance combat with Non Player Characters (NPCs) by allowing them to use weapons and allowing the player to flee, dodge or attack.
Part 2 - Hand To Hand Combat (certainly not)
Combat
Combat can be run in realtime like many first person shooter (FPS) games we see today but in a text adventure it is
simpler to run combat in rounds where each player and NPC gets a turn to shoot, defend or run after seeing what
happened in the previous round.
To start us along we will need to keep track of each player and NPCs stats, the following struct will keep that info, with a map of ennemies for the type and stats of opponents that the player can have encouters with.
type Character struct {
Name string
Health int
Evasion int
Alive bool
Speed int
Weap int
Npc bool
}
func (p *Character) Equip(w int) {
p.Weap = w
}
func (p *Character) Attack() int {
return Weaps[p.Weap].Fire()
}
var ennemies = map[int]*Character{
1: {Name: "Klingon", Health: 50, Alive: true, Weap: 2},
2: {Name: "Romulan", Health: 55, Alive: true, Weap: 3},
}
type Weapon struct {
minAtt int
maxAtt int
Name string
}
func (w *Weapon) Fire() int {
return w.minAtt + rand.Intn(w.maxAtt - w.minAtt)
}
var Weaps = map[int]*Weapon{
1: {Name: "Phaser", minAtt: 5, maxAtt: 15},
2: {Name: "Klingon Disruptor", minAtt: 1, maxAtt: 15},
3: {Name: "Romulan Disruptor", minAtt: 3, maxAtt: 12},
}
type Players []Character
func (slice Players) Len() int {
return len(slice)
}
func (slice Players) Less(i, j int) bool {
return slice[i].Speed > slice[j].Speed //Sort descending
//return slice[i].Speed < slice[j].Speed; //Sort ascending
}
func (slice Players) Swap(i, j int) {
slice[i], slice[j] = slice[j], slice[i]
}
Some explanation about the stats in the struct:
- Health: the number of hit points that the npc or the player will have.
- Alive: Still standing?
- Speed: random number calculated at the beginning of each round of combat to deternine the order in which the NPCs and players move.
- Evasion: Each round the player can choose to evade the attack and this is the amount of HPs by which the attack will be reduced
- Weap: is the weapon the opponents are carrying (Phaser, Klingon/Romulan Disruptor, etc,…)
- Npc: Player or ennemy (true it is an ennemy)
Last time in the maps depicting locations we used a string as a key to the map items but today I am using integers.
The reason for this is that ennemies will be generated randomly so I can now generate a random number between 1 and
the number of ennemies and I will get the key to the ennemy I generated.
Also in there you can see that I created a type called Players and I implemented the Sort interface on it, this is so the combat system can sort characters by their speed to allow faster characters to act first.
Combat Loop
Now that all our groundwork is layed out we can actually start coding the combat system. In the previous article we created the game loop and now we are going to create “combat” loop. The combat loop will run until all parties on one side or the other are dead. Here is the beginner combat loop:
func RunBattle(players Players) {
round := 1
numAlive := players.Len()
for {
DisplayInfo("Combat round", round, "begins...")
if endBattle(players) {
break
} else {
DisplayInfo(players)
round++
}
}
}
func endBattle(players []Character) bool {
count := make([]int, 2)
count[0] = 0
count[1] = 0
for _, pla := range players {
if pla.Alive {
if pla.Npc == false {
count[0]++
} else {
count[1]++
}
}
}
if count[0] == 0 || count[1] == 0 {
return true
} else {
return false
}
}
In case you were wondering this intial code is an endless loop, do not worry about that for now, I simply wanted to illustrate that each round will end by checking to see if all players on one side or the other are dead to determine if the battle is over. The check for endBattle creates an array of 2 integers to keep track of the death on both sides. After looping thru all the characters 1 side or both are zero the battle is over and we can end the combat loop and determine which side won.
Target Selection
Now we need to loop thru the players one at a time and select a target to fire on so that we do not fire on allies and vice versa. In order to determine that we will send the index of the current charater and the full list to a function that will loop thru all the remaining players detect if they are allied or ennemies and pick the first ennemy to fire on.
func selectTarget(players []Character, x int) int {
y := x
for {
y = y + 1
if y >= len(players) {
y = 0
}
if (players[y].Npc != players[x].Npc) && players[y].Alive {
return y
}
if y == x {
return -1
}
}
return -1
}
With all that in place let see the new combat loop
func RunBattle(players Players) {
sort.Sort(players)
round := 1
numAlive := players.Len()
for {
DisplayInfo("Combat round", round, "begins...")
for x := 0; x < players.Len(); x++ {
if players[x].Alive != true {
continue
}
tgt := selectTarget(players, x)
if tgt != -1 {
DisplayInfo("player: ", x, "target: ", tgt)
attp1 := players[x].Attack()
players[tgt].Health = players[tgt].Health - attp1
if players[tgt].Health <= 0 {
players[tgt].Alive = false
numAlive--
}
DisplayInfo(players[x].Name+" attacks and does", attp1, "points of damage with his", Weaps[players[x].Weap].Name, "to the ennemy.")
}
}
if endBattle(players) {
break
} else {
DisplayInfo(players)
round++
}
}
}
In this fairly complete combat loop we can now see that each character is given a turn (if he/she is Alive?) and the target is selected and then fire on based on the weapon each one holds. The next step is to allow the player to either run, defend or attack at each round.
Now let’s add some choices to what the player can do during combat. A player can choose during each round to “Run” or escape the combat, Evade the ennemy’s attacks or Attack. SO during the combat loop when it is the player’s turn we will ask what they want to do and then act on that choice. Here is the new combat loop and the function to get the user inputs.
func RunBattle(players Players) {
sort.Sort(players)
round := 1
numAlive := players.Len()
playerAction := 0
for {
for x := 0; x < players.Len(); x++ {
players[x].Evasion = 0 // Reset evasion for all characters
}
DisplayInfo("Combat round", round, "begins...")
for x := 0; x < players.Len(); x++ {
if players[x].Alive != true {
continue
}
playerAction = 0
if !players[x].Npc {
DisplayInfo("DO you want to")
DisplayInfo("\t1 - Run")
DisplayInfo("\t2 - Evade")
DisplayInfo("\t3 - Attack")
GetUserInput(&playerAction)
}
if playerAction == 2 {
players[x].Evasion = rand.Intn(15)
DisplayInfo("Evasion set to:", players[x].Evasion)
}
tgt := selectTarget(players, x)
if tgt != -1 {
DisplayInfo("player: ", x, "target: ", tgt)
attp1 := players[x].Attack()
players[tgt].Health = players[tgt].Health - attp1
if players[tgt].Health <= 0 {
players[tgt].Alive = false
numAlive--
}
DisplayInfo(players[x].Name+" attacks and does", attp1, "points of damage with his", Weaps[players[x].Weap].Name, "to the ennemy.")
}
}
if endBattle(players) || playerAction == 1 {
break
} else {
DisplayInfo(players)
round++
}
}
}
func DisplayInfof(format string, args ...interface{}) {
fmt.Fprintf(Out, format, args...)
}
func DisplayInfo(args ...interface{}) {
fmt.Fprintln(Out, args...)
}
func GetUserInput(i *int) {
fmt.Fscan(In, i)
}
As you can see we clear Evasion for all characters then once it is determined that a character is not an NPC we ask him/her what they want to do. If the selection is Evade, then we set Evasion for that character for that round. at the end of the round we will not check for the player that wished to flee (yes he still has to be there until the round ends.
Perhaps you have or have not noticed but this allows multiple ennemies to be present for the battle and each one has a chance to kill the player :=)
Final words
In this segment this week I have isolated the code that send displays to the user and receives their input. I did
this so we can change the user input and outputs at will so that we could eventually turn this into a web game. You
can see how this is done in the Gist of this week’s code.
I hope you enjoyed this second instalment of writing a text adventure in Go and I look forward to writing and having you
read and comment on part 3 next week, when we will integrate the combat system into week 1’s Locations and movements,
add items to pick up and use and externalize the game elements so that anyone can change the game as they wish. It
should prove to be a busy article :-).
Have a good week!!!
<<< Part 1: Location and Movement Part 3: Items and Treasure >>>
*** Sign up for my email list to keep in touch with all the interesting new happenings in the go community with the GolangNewsFeed