Python 170 - Two Players
Let’s think about a two player alternating game and how we might do it. A bit of design thinking seems to pay off.
In the early days of XP, one common misunderstanding was the mistaken idea that XP did no design at all, or no design before coding, because we certainly did move to coding very early in the process. The fact is, we design all the time, and we let our code participate in the design sessions as early as we reasonably can. In this article, I’ll speculate about a design change for a two player Asteroids game, spending just a little time. Tomorrow, we’ll turn to coding.
This will be a bit chaotic, because, at least for me, design speculation is quite random, as ideas come and go, get enhanced, torn up, reassembled, discarded, or replaced.
What do we want in terms of a two-player game?
- Two score displays, one on left of screen, one on right.
- Players alternate as long as both have ships left.
- When it’s a player’s turn, their score flashes or somehow indicates it’s their turn.
- When one player’s ships run out, the other player continues until their ships run out.
- In a one-player game, display only the left hand score.
My first thought, which I may no longer agree with, was to have a new object, Players, that held on to one or two pairs of ScoreKeeper and ShipMaker. We’d change out the pairs on each turn. But since we want both scores to appear, we’d have to somehow at least draw the unused Keeper inside the Players object. I had in mind that when a Ship died, it would remove itself, instantiate the Player object, which would inject the other ScoreKeeper and ShipMaker (or the same ones, if there was only one player left), and then things would continue as before.
I like the idea of having two ShipMakers alternating in space, since each one would automatically keep track of its own count of ships. What about ScoreKeeper? If they are both in space, then they could draw correctly, but they would both see each Score instance. How would they know which one should consume the score?
Looking toward the unknowable future, we could imagine a two-controller game in which both players fly at the same time, competing for shooting asteroids, or each other. In that kind of a game, if there were separate ScoreKeepers, the Score instances would have to know who had created them. That would be easy enough, I suppose, and we could key the Keepers to their corresponding Score creator somehow.
Let’s look at scoring briefly, just to see whether we want to think about that.
Scores are emitted by the objects that are destroyed, such as asteroids:
class Asteroid(Flyer):
def interact_with_missile(self, missile, fleets):
if missile.are_we_colliding(self.position, self.radius):
self.score_and_split(missile, fleets)
def score_and_split(self, missile: Missile, fleets):
missile.ping_transponder("ship", self.score_points, fleets)
self.split_or_die(fleets)
def score_points(self, fleets):
fleets.append(Score(self._score))
If the two ships had different names, then the missile might have a ship name as part of its ping logic, which could return as a signature from the ping message or something. Then that would be plugged into the Score and each ScoreKeeper would only accept scores for one ship name.
Seems doable but a bit messy. If both players are to be on screen at the same time, however, something like that would be necessary. It is not necessary for our current story, so perhaps we shouldn’t borrow trouble from the future. We’ll keep the concern in mind, however, because it would be ideal if we could keep the two ScoreKeepers in place all the time.
Or …
One possibility for us now would be to keep both Keepers in place but turn them on and off depending on whose turn it is. But it would truly be nice if two players on screen at the same time would work.
Backing away a bit …
Clearly we could write a new ScoreKeeper that manages more than one score. I’d prefer to use the existing one twice, with some simple extension to decide where to draw its particular score. I’d rather not key the keeper to the player but if we were to allow both players at once, that might be necessary. Putting that off would feel better to me.
What would feel ideal to me would be to add one more simple object, change the interactions a bit, and then we have a one- or two-player game, keeping most of the other objects much as they were. But we have to recognize that the special objects like ShipMaker are presently custom-made for their situation, so might require some changes.
Wondering how ShipMaker works …
Let’s review ShipMaker. It notices there being no ship and when there is no ship, ticks its timer, and when the timer times out, it creates a new ship:
class ShipMaker(Flyer):
def tick(self, delta_time, fleets):
if self._need_ship and not self._game_over:
self._timer.tick(delta_time, self.create_ship, fleets)
def create_ship(self, fleets):
if not self._safe_to_emerge:
return False
if self.ships_remaining > 0:
self.ships_remaining -= 1
fleets.append(Ship(u.CENTER))
else:
fleets.append(GameOver())
self._game_over = True
return True
Ah. If we use two ShipMakers, one for each player, we wouldn’t want to append a GameOver instance. Unless … suppose we didn’t append a GameOver but a PlayerGameOver. And suppose there was another object, maybe Players or PlayerSelector, that watched for those and swapped players, only emitting the real GameOver when all players have received PlayerGameOver.
What am I doing right now?
What I am doing, in case it isn’t obvious, is designing. I’m imagining objects and how they might interact to give us what we want. This is quite speculative at this point, and we should probably try some experiments soon. I would do this kind of thinking for any program I was working on, and would draw some pictures or toss cards around if there were anyone here to look at them with me.
Particularly with this open collaboration design we have here, we need to think about what objects are in the mix at a given time, what they watch, and what they do.
Iterating …
Let’s go back to my starting notion. Suppose we have this PlayerSelector thing in the mix, and that it holds two ShipMaker instances, and remembers which one is “next”. There would be no ShipMaker in the mix normally, unlike today. The PlayerSelector watches to see when there is no Ship and no ShipMaker, and when that happens, it rezzes the next ShipMaker. (That makes the PlayerSelector satisfied, because next time around it’ll see a ShipMaker.)
When a ShipMaker runs, it either creates a Ship or a PlayerGameOver, then it removes itself. If it creates a ship, the PlayerSelector is satisfied and remains quiet. If it creates a PlayerGameOver … or maybe just removes itself … the PlayerSelector is unsatisfied and creates the other ShipMaker … I like having a PlayerGameOver, because when it sees a PlayerGameOver, the PlayerSelector can mark the current Player as done, and when all players are done, it can rez GameOver.
A plan begins to form …
We can create a new Flyer, PlayerSelector, which when it is created generates one or more ShipMaker instances. It works as above, working kind of like a ShipMaker, noting when there are no ships. When there are no ships, it tosses the next ShipMaker into the mix.
If we did that … oh wow wait …
A partially good idea …
Let’s pretend that ShipMaker had an array of ships, not just a number that it used to count ships. If it loaded its array with alternating ships for two players, the game would alternate. That might get a bit weird with free ships. OK, two counters instead of one, and alternating between them.
And maybe we have two ScoreKeepers … and when we rez a new ship, we also rez a signal to the scorekeepers telling them which one should score, #1 or #1, based on the player.
Starts feeling good …
ScoreKeeper gets a key, #1 or #2, and only scores after seeing a ScoreKeeperSignal with that number. We rez two ScoreKeepers, or we extend one to keep two scores. I like having two, it is more in the spirit of the design. They’ll be created to know where to display.
ShipMaker is extended to keep two ship counts and to alternate between them. When it detects a missing ship, after its discreet delay, it creates a new ship and a ScoreKeeperSignal reflecting which player the ship belongs to.
If this idea holds water, ScoreKeeper gets a couple of very simple changes, we add a new ScoreKeeperSignal object and we extend ShipMaker to keep track of ship count for two players. And that should do it.
Summary
A bit of design thinking seems to have paid off. We’ll let this idea perk and perhaps work on it as soon as tomorrow. It’s definitely time to start letting the code take part in the design process.
See you next time!