Dungeon 83
Input from Bruce. And our turn-based implementation isn’t quite good enough yet. I’m not sure what to do about that.
The ever-welcome Bruce Onder suggested:
it might look better if your monsters had two different animation reels - one for idle state and one for moving.
Also, it would be awesome to get some music up in this business. Maybe something introspective for adventuring/moving, and something dashing for combat?
I replied:
agreed on music. if i were doing a game, i;do worry more about animation, but there’d not be much to learn about plugging in different animations.
Bruce, in that way that he has, replied:
For simple two frame animations I’d agree. Anyway, just a thought. :)
And I closed with this:
at least one big issue is that i’d have to get new monsters somewhere. :)
Despite my supposing that the conversation was now over, Bruce was right back with this:
I’m no longer advocating this as I think there are more interesting programming challenges than this. However, I can’t resist sharing this link to free spritesheets for roguelikes/lights: Kenney.nl
Now it happens that I’ve long been a customer and supporter of Kenney. The assets there are mostly lower-res than I feel I want, but they have a very wide range of items. In any case, as I was indicating in the exchange with Bruce, I’m not here to create a viable game so much as to discover whether we can evolve the architecture for a game like this one, never getting seriously blindsided by the changes that come up.
So far, in all the games I’ve done, evolutionary development has gone quite well, although I did throw away the first version of this program, back around article #23 or something.
Musing …
Musing a bit about Bruce’s input, I come from a world where a fantastic game went like this:
YOU ARE STANDING AT THE END OF A ROAD BEFORE A SMALL BRICK BUILDING. AROUND YOU IS A FOREST. A SMALL STREAM FLOWS OUT OF THE BUILDING AND DOWN A GULLY.
In those days, men were men and princesses were princesses and we didn’t need no 4K video. No, we had imagination. We had creativity. We had ASR-33 teletypes. Kids today, with their fancy virtual reality and hyper-realistic graphics, they don’t know what real gaming even is. Why I remember one time …
But I digress. We’re here to see what happens when we write software, and in recent times, that software just happens to take the form of one or another game.
And we have a problem.
Turn-based Issue
I’ve decided to go to a turn-based system, because I want a turn-based combat model, and there’s a problem with the turn-based bit we put in yesterday.
It all started out quite nicely. I removed the randomly-timed monster moving logic. I caused the player code not to move when a monsters can move flag was wet, and to call back to the GameRunner when the player’s move was completed:
function Player:moveBy(aStep)
if not self.runner:monsterCanMove() then return end
self.runner:clearTiles()
self.tile = self.tile:legalNeighbor(self,aStep)
self.tile:illuminate()
self.runner:playerHasMoved()
end
That monsterCanMove
needs a better name, by the way. It really means playerCantMove
.
When the GameRunner gets that message, it moves the monsters:
function GameRunner:playerHasMoved()
self:moveMonsters()
end
function GameRunner:moveMonsters()
for k,m in pairs(self.monsters) do
m:chooseMove()
if math.random() > 0.67 then m:chooseMove() end
if math.random() > 0.67 then m:chooseMove() end
end
end
The moveMonsters
moves each monster at least once, and possibly two or three times.
Now about that flag. In the code I just showed, we don’t even set it. The monsters essentially move instantly after the player hits a button, which implements turn-by-turn behavior pretty much for free.
The problem occurs when there’s an encounter. When a monster tries to step into the player’s tile (or vice versa), an encounter starts. That’s a long-running coroutine, operating on a pull basis by the Floater object that displays the battle description above the action.
We can’t allow the player to move during this encounter. If we do, she could run away, and the battle would look really weird as the monsters stand there battering an empty square, and the commentator babbling about a battle that’s clearly not going on.
Yesterday, I tried to address this concern, and ran into trouble. I put the “can’t move” flag in, with a better name. I turned the flag to can’t move before I moved the monsters, and turned it off when I was done. That worked, except that the player could move during a battle, because the monster moves were done by the time the encounter started.
So I added flag setting to the encounter. And it still didn’t work, because the encounter starts before the monster moves are over. So it sets “can’t move”, and then GameRunner clears it.
So that’s our problem. Change things so that the player can always move when she should be able to, and can’t move when she shouldn’t. And make sure she never gets stuck unable to move. And do it all with style, grace, and a dash of panache.
Options
Often, I go with the first idea I have, trusting that it’s probably enough to let me get close enough to a solution that a better idea will come along if needed. I think that ship has saiiled, yesterday. So today, let’s at least think briefly about some ways we could do this.
- Increment-Decrement
- We could have our “can’t move flag be a counter, counted up on Don’t Move, and back down on OK, Move If You Wish. Then if an encounter were to start while the monsters are moving, the counter would go up to two, and when the monster moving stopped, it would just go down to 1 and motion would still be disallowed until the encounter also agrees.
-
An issue with that is that we might fail to clear the flag, and if that happened, the game would lock up. Arguably, however, this will happen with any locking mechanism and we can’t discount it for that. Unless one of the ideas below doesn’t have the concern.
- A Turn Object
- I’m not sure what I mean by this, but imagine an object Turn that executes just one player-monster turn. In this object we’d set the flag, or maybe the Turn would be the flag. Anyway, the Turn would clear the flag when it was over, so it should be pretty safe.
-
Here–and probably all through this–an issue is Combat. Is Combat just a subtype of Turn? Or does a regular turn have Combat sub-turns? The former seems better. This scheme, if it makes any sense, could eliminate nesting, and might make the game better by offering different kinds of Turns depending on what’s going on. Maybe a Loot-collecting turn or problem-solving Turn.
-
Sounds interesting. Does it mean anything? I’m not sure yet.
- Semaphore
- A more formal implementation of the Increment-Decrement idea would be to build a Semaphore object, which, if you squint a bit, is just a wrapper around a boolean or counter.
- Token
- Perhaps there could be a thing called a MoveToken, and either the player or the monsters would “have” the token. Whoever has the token can move or take other actions, and when he’s done, he gives the token back, and it’s given to the other side. This is analogous to only talking when you’re holding the conch shell.
Issues, Meta and Other
I want to digress to touch on a couple of issues, including, first, a meta-issue.
Meta
I take these articles through these long wandering thought cycles for at least three reasons:
- The thinking is useful, and writing the thoughts helps me.
- I am committed to making these articles reflect how I really program, so thought-cycles are a part of that that needs to be represented.
- I hope you find it useful to see that at least one programmer in the world doesn’t automatically know the One Right Answer to everything, so that you’ll know that you don’t have to know it either.
Issues
- Multiple Monster Moves
- Moving the monsters multiple times, instead of one move of multiple spaces, means that a monster might attack the player twice or three times. That will try to trigger multiple encounters. That’s bad. Right now they get ignored, but even ignored there’s a chance they’ll impact our turn-based logic.
-
Moving multiple squares in one move might help. But there can be more than one monster and each one might attack!
- Encounter Will Be Replaced
- When we (finally) get to turn-based combat, the current encounter, with its long script of actions, will be replaced with a single combat turn that allows for a much more limited range of activities, and that will involve only one side, either player or monster(s). (We might wind up allowing monsters to remain as separate active entities, but the effect will be the same, only one entity acting at a time.
-
This may mean that our needs for this turn-controlling mechanism will change. Whatever we do now, it shouldn’t be a big commitment. The future is fuzzy.
- Maybe Combat is a Choice
- Presently, if you move onto another entity’s square, that initiates a combat encounter. What if we didn’t do that? What if, when you try to move onto another entity’s square, you just can’t? If your move has ended, there you are. If not, maybe you’ll move somewhere else.
-
Then, if you’re the monster, you can choose to attack the player, if she’s “in range” for your attack. That could instantly run one Combat turn. Similarly, if you’re the player, and you find yourself adjacent to some monsters, you can attack one of them, or try to move away. Again, either you take a move turn or a Combat turn.
A Wild Conclusion Has Appeared
Hm. I’m glad we had this little talk. This has given me a plan. Instead of dealing with the sticky issue of stopping the player from moving when she shouldn’t, and the starting and stopping of multiple encounters and the like, let’s just go ahead with a new kind of combat, embodying a single turn.
First Cut at Combat
In the fullness of time, we’ll roll for surprise, and decide who gets to go first. For now, we’ll just let the princess start all combat, but once that’s in, we’ll let the monsters do it as well.
We’ll do a very simple form of combat for now. The attacker attempts a hit on the defender, who has a small chance of avoiding the hit, and who otherwise takes damage dependent on the strength of the attacker. This will surely be an inferior kind of combat, but I think it’ll be “easy” to make it better.
But The Issue Is …
This will be a step backward for the game. One of my fundamental principles of success in software development is to always be inching the product forward, so that the Powers That Be can always see it getting better, and never see it getting worse. I believe this gives us the best chance of them staying happy and always wanting more.
So I really don’t want to toss out the old combat form. Maybe that’s OK. To toss it out, we’d change some of the entries in the TileArbiter’s table of actions. If we leave them in, and you move onto the player’s square, an old-style encounter will ensue. We’ll leave that working until we think it’s OK to move over to the new scheme.
Now all our product people agree that we want to go to this new form of Combat, so I’m OK going ahead with it. It is what they think they want.
Well, we’re 2000 words in. Time to get to work, innit.
Combat Thing
Let’s trigger our new Combat Thing from the Fight button we build a few days back. It works like this:
function GameRunner:createCombatButtons(buttons)
table.insert(buttons, Button:textButton("Flee", 100,350))
table.insert(buttons, Button:textButton("Fight",200,350))
end
function Button:textButton(label, x, y, w, h)
local w = w or 64
local h = h or 64
local img = Button:createTextImage(label,w,h)
return Button(string.lower(label), x, y, w, h, img)
end
function Button:touched(aTouch, player)
if aTouch.state ~= BEGAN then return end
local x = aTouch.pos.x
local y = aTouch.pos.y
local ww = self.w/2
local hh = self.h/2
if x > self.x - ww and x < self.x + ww and
y > self.y - hh and y < self.y + hh then
player[self.name](player)
end
end
function Player:fight()
end
There we are, just the place to initiate Combat. (I note that while the move buttons send the playerHasMoved
message to GameRunner, these do not. We should move that call down to the end of this function here, I think. Let’s do that, test, and commit.
function Button:touched(aTouch, player)
if aTouch.state ~= BEGAN then return end
local x = aTouch.pos.x
local y = aTouch.pos.y
local ww = self.w/2
local hh = self.h/2
if x > self.x - ww and x < self.x + ww and
y > self.y - hh and y < self.y + hh then
player[self.name](player)
end
self.runner:playerHasMoved()
end
Commit: all player touches report player has moved.
I’m a bit conflicted about this, as this decision should perhaps not be made by something as generic as a button. For now, I think it’ll do what we need.
Well, if it would work at all:
Button:61: attempt to index a nil value (field 'runner')
stack traceback:
Button:61: in method 'touched'
GameRunner:322: in method 'touched'
Main:38: in function 'touched'
The button doesn’t know the runner. We do know the player. Let’s send her a message:
function Button:touched(aTouch, player)
if aTouch.state ~= BEGAN then return end
local x = aTouch.pos.x
local y = aTouch.pos.y
local ww = self.w/2
local hh = self.h/2
if x > self.x - ww and x < self.x + ww and
y > self.y - hh and y < self.y + hh then
player[self.name](player)
end
player:hasMoved()
end
function Player:hasMoved()
self.runner:playerHasMoved()
end
Now then …
OK, there’s an issue here, which is that the button doesn’t know whether the player is listening to buttons or not. So if we press a button, we give the monsters another move, even if the button doesn’t take.
We’ve got too many things going on at once here. Our new turn-based idea is a bit weak, no surprise, but the steps to make it solid enough are not in place.
And I’ve had another idea:
A Turn Object
Yes, we talked about this, but my current idea is actually to do it.
Let’s undo that last commit, and revert it out, and go again.
Let’s give the GameRunner an array of Turn objects. There will be two kinds, a Player one and a Monsters one. We’ll iterate between the two. We’ll use whichever one is active as an indicator of who can do things.
I think this will be easy. (I have a bad record of thinking that, however.) We’ll learn something, and we can consider this a spike to figure out how to better handle our turn-based approach.
I have an idea how this will work, and I’m just going to go for it. No TDD. I accept that I’m a bad person.
function GameRunner:init()
self.tileSize = 64
self.tileCountX = 85 -- if these change, zoomed-out scale
self.tileCountY = 64 -- may also need to be changed.
self.tiles = {}
for x = 1,self.tileCountX+1 do
self.tiles[x] = {}
for y = 1,self.tileCountY+1 do
local tile = Tile:edge(x,y, self)
self:setTile(tile)
end
end
self.cofloater = Floater(self, 50,25,4)
self:createTurns()
end
And …
function GameRunner:createTurns()
self.turns = {PlayerTurn(self), MonsterTurn(self)}
end
And …
MonsterTurn = class()
function MonsterTurn:init(runner)
self.runner = runner
end
PlayerTurn = class()
function PlayerTurn:init(runner)
self.runner = runner
end
Now we’ve got ‘em, what shall we do with them? Well, we need to switch between them:
function GameRunner:createLevel(count)
self:createRandomRooms(count)
self:connectRooms()
self:convertEdgesToWalls()
local r1 = self.rooms[1]
local rcx,rcy = r1:center()
local tile = self:getTile(vec2(rcx,rcy))
self.player = Player(tile,self)
self.monsters = self:createThings(Monster,9)
for i,monster in ipairs(self.monsters) do
monster:startAllTimers()
end
self.keys = self:createThings(Key,5)
self:createThings(Chest,5)
self:createLoots(10)
self.buttons = {}
table.insert(self.buttons, Button("left",100,200, 64,64, asset.builtin.UI.Blue_Slider_Left))
table.insert(self.buttons, Button("up",200,250, 64,64, asset.builtin.UI.Blue_Slider_Up))
table.insert(self.buttons, Button("right",300,200, 64,64, asset.builtin.UI.Blue_Slider_Right))
table.insert(self.buttons, Button("down",200,150, 64,64, asset.builtin.UI.Blue_Slider_Down))
self:createCombatButtons(self.buttons)
self:runCrawl(self.initialCrawl, false)
end
We’ll initialize an index, and a member variable turn that I think we’ll find useful.
self:createCombatButtons(self.buttons)
self.turnIndex = #self.turns
self.turn = self:nextTurn()
self:runCrawl(self.initialCrawl, false)
function GameRunner:nextTurn()
self.turnIndex = self.turnIndex + 1
if self.turnIndex > #self.turns then self.turnIndex = 1 end
return self.turns[self.turnIndex]
end
Now when we execute button code we really want to know whether we are in the PlayerTurn.
function Button:touched(aTouch, player)
if aTouch.state ~= BEGAN then return end
if not player:itsOurTurn() then return end
local x = aTouch.pos.x
local y = aTouch.pos.y
local ww = self.w/2
local hh = self.h/2
if x > self.x - ww and x < self.x + ww and
y > self.y - hh and y < self.y + hh then
player[self.name](player)
end
player:turnOver()
end
Now player needs those two methods (and probably not the hasMoved one).
function Player:itsOurTurn()
return self.runner:itsPlayerTurn()
end
function Player:turnOver()
self.runner:turnOver()
end
... ummm
This isn’t quite right. TDD might have helped after all, but I think not, as we are well into the GameRunner just running. I want each Turn to have an execute command, and I want the function above to make it happen. Let’s see …
And that method should be turnComplete
. turnOver
is ambiguous.
function GameRunner:turnComplete()
self.tile = self:nextTurn()
self.tile:execute(self)
end
And in create level:
...
self:createCombatButtons(self.buttons)
self.turnIndex = #self.turns
self:runCrawl(self.initialCrawl, false)
self.turn = self:turnComplete() -- start things going
end
We’ll crash now looking for execute, I hope.
GameRunner:341: attempt to call a nil value (method 'execute')
stack traceback:
GameRunner:341: in method 'turnComplete'
GameRunner:88: in method 'createLevel'
Main:18: in function 'setup'
Player’s is empty. Monster … should move the monsters …
function MonsterTurn:execute(runner)
runner:moveMonsters()
runner:turnComplete()
end
Could this possibly work? Possibly. In fact, general motion works OK, and battles commence as before, but monsters never move until we move the princess.
This is as expected, but honestly I don’t like it. It was more menacing when, if she stood there indecisive, they came closer and closer. We could give her a move timer, and move them every now and again, I suppose. But that’s not for just now, is it?
Let me play a bit more and be sure this mostly works. I find that the buttons ask for itsOurTurn
. Keyboard should do that as well, and we should implement the method.
function GameRunner:itsPlayerTurn()
return self.turn:isPlayerTurn()
end
function MonsterTurn:isPlayerTurn()
return false
end
function PlayerTurn:isPlayerTurn()
return true
end
Hm, I’m getting this walkback:
GameRunner:244: attempt to index a nil value (field 'turn')
stack traceback:
GameRunner:244: in function <GameRunner:243>
(...tail calls...)
Button:53: in method 'touched'
GameRunner:339: in method 'touched'
Main:38: in function 'touched'
That code is:
function GameRunner:itsPlayerTurn()
return self.turn:isPlayerTurn()
end
Somehow ‘turn’ isn’t set. TDD might have helped here after all. I’m basically doing TDD “manually”, which is far from as repeatable and confidence building. Some people call this practice “debugging” and they’re not wrong.
Ah. At least part of my problem is that the keyboard doesn’t toggle turns, so I’ve been running on the old code. We’d best fix that:
function Player:keyPress(key)
if not self:itsOurTurn() then return false end
local step = PlayerSteps[key]
if step then
self:moveBy(step)
end
self:turnComplete()
end
Let’s make it crash again. OK, turn is just not set. But I do see that we executed the player execute, because I put prints in both execute commands.
Ah. In createLevel
:
self.turn = self:turnComplete() -- start things going
That function doesn’t return a value. It just does the thing.
self:turnComplete() -- start things going
Now then, what breaks? The same thing. Does turnComplete
not do what I expect it to? Sheesh!
function GameRunner:turnComplete()
self.tile = self:nextTurn()
self.tile:execute(self)
end
82 articles of typing “tile” and you just can’t stop.
function GameRunner:turnComplete()
self.turn = self:nextTurn()
self.turn:execute(self)
end
Yes, I know that in Rust or something, the compiler would have caught that error. We play the cards we’re dealt here.
Run again. Things are working better now. During normal walking about, princess and monsters take turns, princess first. Monsters can occasionally catch up and when they do, they can start an encounter.
The princess can try to move during the encounter, and if she does, the monsters also get a turn. She seems not to be able to move during the encounter, but it would be nice if an attempt to move didn’t give the monsters another shot.
Why doesn’t she actually move? The Turn thinks it’s her turn: it almost always does at present. Is there still an encounter blocking motion?
function Player:moveBy(aStep)
if not self.runner:monsterCanMove() then return end
self.runner:clearTiles()
self.tile = self.tile:legalNeighbor(self,aStep)
self.tile:illuminate()
self.runner:playerHasMoved()
end
This is old technology. And we should at least somewhat fix it. I think the fix we’ll try is to let the Turns handle it, removing both the checking calls here.
function Player:moveBy(aStep)
self.runner:clearTiles()
self.tile = self.tile:legalNeighbor(self,aStep)
self.tile:illuminate()
end
This might redundantly clear and illuminate, but for now I’m certainly OK with that. I think now she can move away from the encounter while it still plays, but the monsters will be able to follow here if they want to.
With that change in place, the game plays rather well. Yes, you can run away from a battle, sometimes. Usually the monsters will just follow. Sometimes you get a square away but things still look reasonable. I think we can commit this. I’ll remove the prints and commit: turn-based uses Turn objects.
It’s coming up on lunch time, so the session has run about 3 hours. Let’s sum up and publish.
Summary
Yesterday’s foray into turn-based wasn’t bad, but the original flag-based scheme wasn’t robust enough to deal with encounters well. It seemed to me to make more sense to have an actual “Turn” object, amounting to a Token that decides whose turn it is. We’ve begun to give those objects a bit of behavior, in the case of the Monsters, and we’ll probably do something with the Player, although she usually only does things based on user input, so that may not happen in quite the same way as the Monsters.
In any case, motion is now fully turn-based, and the Encounter, which will go away “soon”, works adequately for present purposes. It is a bit different from before, in that you can move the princess during battle, sometimes, but it never looks too weird. Arguably it looks like she’s trying to get away, which is rather the point of moving.
We did 2000 words of thinking, though, which is an awful lot without coding anything. I felt it was worthwhile to think, since yesterday had ended in confusion.
And then I didn’t TDD the Turn functionality at all, not even the swapping back and forth code. At least one of the defects, saying tile
when I meant turn
, would probably have shown up in TDD without needing quite as much manual testing.
There were fiddly bits due to leaving in parts of the old logic. This is trying to tell me something, but what?
Well, when we put in one kind of turn-controlling code, and that doesn’t cause us to automatically run into the old turn-controlling code to replace it, that suggests that the turn control logic isn’t cohesive, but instead that it’s spread all around in the system, so that we don’t see bits of it unless we remember to look in just the right place.
In particular, that suggests to me that the keyboard handling and button handling need to be better managed by the Turns.
What if keyboard and button calls went through GameRunner, who passed them to the Turns? Then the monster turn would probably ignore them, and the player turn would pass them on to be processed or process them in situ, as appropriate.
That’s probably what the code is telling us.
We’ll think about that next time. I hope you’ll visit then.