Kotlin 227 - A Code Review
GitHub Decentralized Repo
GitHub Centralized Repo
Let’s look at cohesion, duplication, and other considerations. Maybe we can get a bit more gilding on this lily.
Yesterday afternoon, I reviewed the code in the centralized version of Asteroids, and aside from a few opportunities to make things private and remove unused methods, it seemed pretty clean. There are a few classes that seem large relative to the others:
81 Asteroid.kt
57 AsteroidView.kt
10 Collider.kt
10 Collision.kt
41 Controls.kt
29 DeferredAction.kt
162 Game.kt
79 Missile.kt
18 OneShot.kt
153 Saucer.kt
83 ScoreKeeper.kt
126 Ship.kt
21 ShotOptimizer.kt
8 SpaceObject.kt
96 SpaceObjectCollection.kt
31 Splat.kt
28 SplatView.kt
44 TemplateProgram.kt
46 Transaction.kt
11 Tween.kt
61 Universe.kt
1295 total
Saucer and Ship seem large, as does Game. All three of them may have an excuse, as they all have a lot of things to do. That said, we ought to look at them and see whether they are cohesive. What do I mean by that?
Well, what I mean to ask is whether the object seems to have just one responsibility, or more than one. When an object has multiple different-seeming responsibilities, we say that it is less cohesive than an objet1 with fewer distinct responsibilities.
We can get another clue about cohesiveness by considering the member variables of the object. If they change, do they all tend to change at the same time and rate, or do some change at very different times than others? If they’re different, that is a strong indicator that the object might not be as cohesive as it could do.
When an object is not cohesive, it’s a sign that there are probably two or more objects that could exist and collaborate to provide the behavior of the original. We would usually break out those objects and then compose our new version out of those smaller ones.
There is another kind of clue we might look for when thinking about whether we have just the right objects. Are there different objects that are all doing (pretty much) the very same thing? If so, perhaps there is a new object waiting to be created, that does that thing, and that all the objects doing that thing could use to get it done.
This second indicator is my personal favorite clue when improving the code: duplication. When I see something being done multiple times, I start looking for an object that can do it.
There’s an example in the code now: Collision.
class Collision(private val collider: Collider) {
private fun hit(other: Collider): Boolean
= collider.position.distanceTo(other.position) < collider.killRadius + other.killRadius
fun executeOnHit(other: Collider, action: () -> Unit) {
if (hit(other)) action()
}
}
This tiny object is created with an instance of Collider (asteroid, missile, saucer, or ship), and given another Collider, will execute a block of code if the two objects are colliding. All four Collider implementors use this object:
class Asteroid
private fun checkCollision(other: Collider, trans: Transaction) {
Collision(this).executeOnHit(other) {
dieDueToCollision(trans)
}
}
class Missile
private fun checkAndScoreCollision(other: Collider, trans: Transaction, score: Int) {
Collision(other).executeOnHit(this) {
terminateMissile(trans)
if (missileIsFromShip) trans.addScore(score)
}
}
class Saucer
private fun checkCollision(collider: Collider, trans: Transaction) {
Collision(collider).executeOnHit(this) {
trans.add(Splat(this))
trans.remove(this)
}
}
class Ship
private fun checkCollision(other: Collider, trans: Transaction) {
Collision(other).executeOnHit(this) {
trans.add(Splat(this))
trans.remove(this)
}
}
We see there’s still some duplication between Saucer and Ship, but I don’t see an easy way to get rid of it. C’est la vie!2 But the Collision object removes and abstracts the distance calculation that is always involved in deciding whether two objects have collided.
Note, though, that this object doesn’t become part of the asteroid or whatever object uses it. It is created on the fly and thrown away after use. We do check collisions a lot, and we could make the case that creating and destroying a few hundred Collisions every 120th of a second is wasteful. And we certainly could gin one up that could be a permanent part of each Collider, since we could build it on this
and pass in other
. Is that worth doing? Perhaps, but I’m not motivated to do it. I am inclined to trust the language to create and destroy small objects at low cost. YMMV.
I think of objects like Collider as “helper objects”, since they are not part of the user object, just something they create as needed. But one way or another, they provide behavior which would otherwise need to be bound into the user object.
Where we we? Oh, right …
Objects with multiple responsibilities suggest new objects to us, as does duplicated behavior across objects. Let’s look in a bit more detail at one of our larger objects, Game, and see what we can see. Game is the largest class we have. Here it is in all its glory. Give it a quick scan and join me below.
class Game(val knownObjects:SpaceObjectCollection = SpaceObjectCollection()) {
private var lastTime = 0.0
private var numberOfAsteroidsToCreate = 0
private var saucer = Saucer()
private lateinit var ship: Ship
private var scoreKeeper: ScoreKeeper = ScoreKeeper(-1)
private val waveOneShot = OneShot(4.0) { makeWave(it) }
private val saucerOneShot = OneShot( 7.0) { startSaucer(it) }
private fun startSaucer(trans: Transaction) {
saucer.start(trans)
}
private val shipOneShot = OneShot(U.SHIP_MAKER_DELAY, { canShipEmerge() }) {
if ( scoreKeeper.takeShip() ) {
startShipAtHome(it)
}
}
private fun startShipAtHome(trans: Transaction) {
ship.setToHome()
trans.add(ship)
}
// all OneShot instances go here:
private val allOneShots = listOf(waveOneShot, saucerOneShot, shipOneShot)
fun changesDueToInteractions(): Transaction {
val trans = Transaction()
knownObjects.pairsToCheck().forEach {
it.first.interactWith(it.second, trans)
it.second.interactWith(it.first, trans)
}
return trans
}
fun createInitialContents(controls: Controls) {
initializeGame(controls, -1)
}
fun insertQuarter(controls: Controls) {
initializeGame(controls, U.SHIPS_PER_QUARTER)
}
private fun initializeGame(controls: Controls, shipCount: Int) {
numberOfAsteroidsToCreate = U.ASTEROID_STARTING_COUNT
knownObjects.performWithTransaction { trans ->
createInitialObjects(trans,shipCount, controls)
}
}
private fun createInitialObjects(
trans: Transaction,
shipCount: Int,
controls: Controls
) {
cancelAllOneShots()
trans.clear()
scoreKeeper = ScoreKeeper(shipCount)
knownObjects.scoreKeeper = scoreKeeper
val shipPosition = U.CENTER_OF_UNIVERSE
ship = Ship(shipPosition, controls)
saucer = Saucer()
}
private fun cancelAllOneShots() {
val ignored = Transaction()
for (oneShot in allOneShots) {
oneShot.cancel(ignored)
}
}
fun cycle(elapsedSeconds: Double, drawer: Drawer? = null) {
val deltaTime = elapsedSeconds - lastTime
lastTime = elapsedSeconds
tick(deltaTime)
beforeInteractions()
processInteractions()
U.AsteroidTally = knownObjects.asteroidCount()
createNewWaveIfNeeded()
createSaucerIfNeeded()
createShipIfNeeded()
drawer?.let { draw(drawer) }
}
private fun createShipIfNeeded() {
if ( knownObjects.shipIsMissing() ) {
knownObjects.performWithTransaction { shipOneShot.execute(it) }
}
}
private fun createNewWaveIfNeeded() {
if ( U.AsteroidTally == 0 ) {
knownObjects.performWithTransaction { waveOneShot.execute(it) }
}
}
private fun createSaucerIfNeeded() {
if ( knownObjects.saucerIsMissing() ) {
knownObjects.performWithTransaction { saucerOneShot.execute(it) }
}
}
private fun beforeInteractions() = knownObjects.saucers().forEach { it.beforeInteractions() }
private fun draw(drawer: Drawer) {
knownObjects.forEachInteracting { drawer.isolated { it.draw(drawer) } }
knownObjects.scoreKeeper.draw(drawer)
}
fun howMany(): Int {
return numberOfAsteroidsToCreate.also {
numberOfAsteroidsToCreate += 2
if (numberOfAsteroidsToCreate > 11) numberOfAsteroidsToCreate = 11
}
}
fun makeWave(it: Transaction) {
for (i in 1..howMany()) {
it.add(Asteroid(U.randomEdgePoint()))
}
}
fun processInteractions() = knownObjects.applyChanges(changesDueToInteractions())
fun tick(deltaTime: Double) {
updateTimersFirst(deltaTime)
thenUpdateSpaceObjects(deltaTime)
}
private fun updateTimersFirst(deltaTime: Double) {
with (knownObjects) {
performWithTransaction { trans ->
deferredActions().forEach { it.update(deltaTime, trans) }
}
}
}
private fun thenUpdateSpaceObjects(deltaTime: Double) {
with (knownObjects) {
performWithTransaction { trans ->
spaceObjects().forEach { it.update(deltaTime, trans) }
}
}
}
fun canShipEmerge(): Boolean {
if (knownObjects.saucerIsPresent()) return false
if (knownObjects.missiles().isNotEmpty()) return false
for ( asteroid in knownObjects.asteroids() ) {
val distance = asteroid.position.distanceTo(U.CENTER_OF_UNIVERSE)
if ( distance < U.SAFE_SHIP_DISTANCE ) return false
}
return true
}
}
Wow, that is huge, isn’t it? Right off the bat, I see a few very distinct responsibilities.
- It creates the starting configurations;
- It runs the central game
cycle
of updating and interacting the objects; - It creates new waves, saucers, and ships when they are needed.
Let’s focus on the member variables in more detail.
class Game(val knownObjects:SpaceObjectCollection = SpaceObjectCollection()) {
private var lastTime = 0.0
private var numberOfAsteroidsToCreate = 0
private var saucer = Saucer()
private lateinit var ship: Ship
private var scoreKeeper: ScoreKeeper = ScoreKeeper(-1)
private val waveOneShot = OneShot(4.0) { makeWave(it) }
private val saucerOneShot = OneShot( 7.0) { startSaucer(it) }
private val shipOneShot = OneShot(U.SHIP_MAKER_DELAY, { canShipEmerge() }) {
if ( scoreKeeper.takeShip() ) {
startShipAtHome(it)
}
}
// all OneShot instances go here:
private val allOneShots = listOf(waveOneShot, saucerOneShot, shipOneShot)
These certainly don’t all vary at the same rate. lastTime
is used on every cycle, to compute the deltaTime
that our objects need. numberOfAsteroidsToCreate
is used only when we create a starting configuration. saucer
is set when we create a new configuration, and used when it is time for the saucer to appear.
Push Thought Stack Down
Why do we hold on to the Saucer? Why not just create a new one? Does the Saucer have some kind of memory that is being preserved? Check its member variables:
class Saucer : SpaceObject, Collider {
override lateinit var position: Point
override val killRadius = U.SAUCER_KILL_RADIUS
private var direction: Double = 1.0
lateinit var velocity: Velocity
private val speed = U.SAUCER_SPEED
private var elapsedTime = 0.0
private var timeSinceSaucerSeen = 0.0
private var timeSinceLastMissileFired = 0.0
var sawShip = false
var shipFuturePosition = Point.ZERO
private var missileReady = true
private var currentMissile: Missile? = null
It turns out that it just has one value that is carried over, direction
, which is used to make the saucer alternate whether it goes right to left or left to right. That aside, I think we could create a fresh saucer every time we need one, and save a long term member in Game.
While we’re at it, let’s look at Ship, because Game holds on to that as well. Why?
class Ship(
override var position: Point,
val controls: Controls = Controls(),
override val killRadius: Double = U.SHIP_KILL_RADIUS
) : SpaceObject, Collider {
var velocity: Velocity = Velocity.ZERO
var heading: Double = 0.0
private var dropScale = U.DROP_SCALE
var accelerating: Boolean = false
var displayAcceleration: Int = 0
I don’t see any reason why we need to use the same Ship over and over. I think that in the past, it might be remembering where it was as a clue to hyperspace, but we don’t do that any more. Let’s try creating a new ship every time.
private fun startShipAtHome(trans: Transaction) {
ship.setToHome()
trans.add(ship)
}
Here, let’s just create a new one. Ah. The issue is that we’re passed controls at the time of game initialization. I think we’d have to hold on to the controls, which might still be better than holding the ship, but that’s a larger change than I want to make quite this causally.
Similarly for Saucer. We could cache a direction and pass it on creation, or we could let it be random. But I don’t see much advantage to doing that.
In both cases, we’d be holding on to something a bit more elementary, which would be good, but not terribly valuable. More to the point, I’m willing to try tiny changes during this assessment, but I’m here looking for larger issues and not to solve them … yet.
Pop Thought Stack
We could probably break out the creation of the game configuration. We could probably break out the game cycle, given a game configuration.
What is a “game configuration”? Certainly most of it is represented in knownObjects
, the SpaceObjectCollection that contains, well, all the objects. What else is needed during play?
Imagine that we created an object that starts at cycle
and does everything in there. Let’s track down what it might need to know.
fun cycle(elapsedSeconds: Double, drawer: Drawer? = null) {
val deltaTime = elapsedSeconds - lastTime
lastTime = elapsedSeconds
tick(deltaTime)
beforeInteractions()
processInteractions()
U.AsteroidTally = knownObjects.asteroidCount()
createNewWaveIfNeeded()
createSaucerIfNeeded()
createShipIfNeeded()
drawer?.let { draw(drawer) }
}
We could let it know lastTime
or change it to receive deltaTime
from game. I think Game would still be the receiver of events from OPENRNDR. Looking on down at methods to see what they know:
private fun beforeInteractions() = knownObjects.saucers().forEach { it.beforeInteractions() }
fun processInteractions() = knownObjects.applyChanges(changesDueToInteractions())
fun changesDueToInteractions(): Transaction {
val trans = Transaction()
knownObjects.pairsToCheck().forEach {
it.first.interactWith(it.second, trans)
it.second.interactWith(it.first, trans)
}
return trans
}
So far all it would need would be knownObjects
. How about the three create methods?
private fun createNewWaveIfNeeded() {
if ( U.AsteroidTally == 0 ) {
knownObjects.performWithTransaction { waveOneShot.execute(it) }
}
}
It would have to have the waveOneShot
, and probably the other two. We’ll find that out for sure below.
private fun createSaucerIfNeeded() {
if ( knownObjects.saucerIsMissing() ) {
knownObjects.performWithTransaction { saucerOneShot.execute(it) }
}
}
Right. And I think that as things stand, it would have to know the saucer and probably the ship, but we know we could pretty easily change things so that those objects can be created new every time. And finally:
private fun createShipIfNeeded() {
if ( knownObjects.shipIsMissing() ) {
knownObjects.performWithTransaction { shipOneShot.execute(it) }
}
}
A plan begins to emerge!
So. We could imagine a GameCycler object that is fed nothing but an initialized SpaceObjectCollection, and that initializes some OneShots, perhaps caches the Ship and Saucer (which could by convention be present in the SpaceObjectCollection, or passed separately, or perhaps get created in the Cycler setup).
Then it would implement cycle
and all the methods below it.
That could be rather nice. It would simplify Game, and GameCycler would be quite cohesive.
Plan
I think we have a plan. Took us 442 lines to get here, but I think our design would be better if we were to break out a GameCycler object from Game. Since we’re here to learn things about Kotlin and design, let’s do it. The question is how we should proceed.
We probably won’t finish this move today. We want to proceed in small steps in any case. I have an idea. Let me explain it a bit, in the hope that I’ll understand it better when I’ve done that.
- Let’s assume that our GameCycler needs the
knownObjects
collection, plus the Controls, and perhaps the Ship and Saucer. - We’ll want to work toward the Game only creating a knownObjects collection for the Cycler to use.
- We’ll create a nearly trivial GameCycler and call it on every
cycle
(perhaps passingdeltatime
rather than elapsed time). - We’ll either create GameCycler to know the Game or we’ll pass it to it on
cycle
. - Initially
GameCycler.cycle
will just call back to Game to do its cycle. - Incrementally, we’ll move code from Game to GameCycle, letting it call back as needed.
- Sooner or later, it won’t need to call back any more.
- Done.
I think this pattern is like the Strangler pattern, which I first learned from Michael Feathers’ classic Working Effectively with Legacy Code.
The Starting Foothold
Let’s see where we can start.
Game starting is actually rather interesting. Let’s look at insertQuarter
, which is triggered by typing “q” on the game screen:
fun insertQuarter(controls: Controls) {
initializeGame(controls, U.SHIPS_PER_QUARTER)
}
private fun initializeGame(controls: Controls, shipCount: Int) {
numberOfAsteroidsToCreate = U.ASTEROID_STARTING_COUNT
knownObjects.performWithTransaction { trans ->
createInitialObjects(trans,shipCount, controls)
}
}
private fun createInitialObjects(
trans: Transaction,
shipCount: Int,
controls: Controls
) {
cancelAllOneShots()
trans.clear()
scoreKeeper = ScoreKeeper(shipCount)
knownObjects.scoreKeeper = scoreKeeper
val shipPosition = U.CENTER_OF_UNIVERSE
ship = Ship(shipPosition, controls)
saucer = Saucer()
}
All that happens here is that we create a clear transaction, which will empty knownObject
entirely. Then we give it a scoreKeeper, which the game retains as well. The game creates a ship and saucer and memorizes them. They are not passed into knownObjects.
What happens after that? OPENRNDR calls cycle
and the cycle code rather quickly notices that there are no asteroids, no saucer, and no ship, and triggers its one-shots to create them after suitable intervals. Everything unfolds from there.
I notice that we’ll probably have to pass numberOfAsteroidsToCreate
into GameCycler.
So let’s add code to create our Cycler:
class Game
private lateinit var cycler: GameCycler
private fun createInitialObjects(
trans: Transaction,
shipCount: Int,
controls: Controls
) {
cancelAllOneShots()
trans.clear()
scoreKeeper = ScoreKeeper(shipCount)
knownObjects.scoreKeeper = scoreKeeper
val shipPosition = U.CENTER_OF_UNIVERSE
ship = Ship(shipPosition, controls)
saucer = Saucer()
cycler = GameCycler(knownObjects, numberOfAsteroidsToCreate, ship, saucer)
}
IDEA would like to create this class for us.
class GameCycler(
knownObjects: SpaceObjectCollection,
numberOfAsteroidsToCreate: Int,
ship: Ship,
saucer: Saucer) {
}
We should be green. Commit: add GameCycler class.
Let’s call it and let it call us back. To do that, we’ll have to extract code from Game.cycle
so that Cycler will have something to call:
fun cycle(elapsedSeconds: Double, drawer: Drawer? = null) {
stranglerCycle(elapsedSeconds, drawer)
}
fun stranglerCycle(elapsedSeconds: Double, drawer: Drawer?) {
val deltaTime = elapsedSeconds - lastTime
lastTime = elapsedSeconds
tick(deltaTime)
beforeInteractions()
processInteractions()
U.AsteroidTally = knownObjects.asteroidCount()
createNewWaveIfNeeded()
createSaucerIfNeeded()
createShipIfNeeded()
drawer?.let { draw(drawer) }
}
Should be green. Commit: make strangler method.
I think I want GameCycler to have deltaTime. Let’s refactor time back:
fun cycle(elapsedSeconds: Double, drawer: Drawer? = null) {
val deltaTime = elapsedSeconds - lastTime
lastTime = elapsedSeconds
stranglerCycle(deltaTime, drawer)
}
fun stranglerCycle(deltaTime: Double, drawer: Drawer?) {
tick(deltaTime)
beforeInteractions()
processInteractions()
U.AsteroidTally = knownObjects.asteroidCount()
createNewWaveIfNeeded()
createSaucerIfNeeded()
createShipIfNeeded()
drawer?.let { draw(drawer) }
}
Test. Green. Commit: game computes deltaTime before using cycler.
Now call cycler and have it call right back:
fun cycle(elapsedSeconds: Double, drawer: Drawer? = null) {
val deltaTime = elapsedSeconds - lastTime
lastTime = elapsedSeconds
cycler.cycle(this, deltaTime, drawer)
}
IDEA wants to create the method:
fun cycle(game: Game, deltaTime: Double, drawer: Drawer?) {
game.stranglerCycle(deltaTime, drawer)
}
I think this should run green. I am mistaken. Two tests fail because cycler isn’t initialized.
Gol durn3 lateinit
things. I do this, hating it:
private var cycler: GameCycler = GameCycler(knownObjects, 0, Ship(U.CENTER_OF_UNIVERSE), saucer)
Tests should be happy now. They are. Commit: GameCycler cycles game (using stranglerCycle).
Reflection
OK, our strangler is in place and running. We can now begin to move functionality over from Game into GameCycler. The first item is tick. What does that do?
fun tick(deltaTime: Double) {
updateTimersFirst(deltaTime)
thenUpdateSpaceObjects(deltaTime)
}
private fun updateTimersFirst(deltaTime: Double) {
with (knownObjects) {
performWithTransaction { trans ->
deferredActions().forEach { it.update(deltaTime, trans) }
}
}
}
private fun thenUpdateSpaceObjects(deltaTime: Double) {
with (knownObjects) {
performWithTransaction { trans ->
spaceObjects().forEach { it.update(deltaTime, trans) }
}
}
}
Let’s do this in the smallest way we can think of, just moving tick
and calling back for the others. If we do that, we’ll have a need for the game
to be passed around. Probably we should move it into our constructor but let’s discover that need and then do it.
Either IDEA doesn’t know how to help me or I don’t know how to ask. I’ll cut the method from Game and paste it to GameCycler:
class GameCycler(knownObjects: SpaceObjectCollection, numberOfAsteroidsToCreate: Int, ship: Ship, saucer: Saucer) {
fun cycle(game: Game, deltaTime: Double, drawer: Drawer?) {
game.stranglerCycle(deltaTime, drawer)
}
fun tick(deltaTime: Double) {
updateTimersFirst(deltaTime)
thenUpdateSpaceObjects(deltaTime)
}
}
Of course we don’t have those methods. I was going to call back for them but the fix is too obvious, I’ll just move them over.
class GameCycler(private val knownObjects: SpaceObjectCollection, numberOfAsteroidsToCreate: Int, ship: Ship, saucer: Saucer) {
fun cycle(game: Game, deltaTime: Double, drawer: Drawer?) {
game.stranglerCycle(deltaTime, drawer)
}
fun tick(deltaTime: Double) {
updateTimersFirst(deltaTime)
thenUpdateSpaceObjects(deltaTime)
}
private fun updateTimersFirst(deltaTime: Double) {
with (knownObjects) {
performWithTransaction { trans ->
deferredActions().forEach { it.update(deltaTime, trans) }
}
}
}
private fun thenUpdateSpaceObjects(deltaTime: Double) {
with (knownObjects) {
performWithTransaction { trans ->
spaceObjects().forEach { it.update(deltaTime, trans) }
}
}
}
}
And I need to do the tick:
fun cycle(game: Game, deltaTime: Double, drawer: Drawer?) {
tick(deltaTime)
game.stranglerCycle(deltaTime, drawer)
}
fun cycle(elapsedSeconds: Double, drawer: Drawer? = null) {
val deltaTime = elapsedSeconds - lastTime
lastTime = elapsedSeconds
cycler.cycle(this, deltaTime, drawer)
}
fun stranglerCycle(deltaTime: Double, drawer: Drawer?) {
beforeInteractions()
processInteractions()
U.AsteroidTally = knownObjects.asteroidCount()
createNewWaveIfNeeded()
createSaucerIfNeeded()
createShipIfNeeded()
drawer?.let { draw(drawer) }
}
I think we should be green. Darn, we aren’t. We have tests that want to do game.tick.
There’s just one test. I think we’ll disable it for now. Remind me to put it back. With that, we’re green. Commit: move tick
to GameCycler. Test commented out.
Let’s do one more, then I have an errand to run.
class GameCycler(private val knownObjects: SpaceObjectCollection, numberOfAsteroidsToCreate: Int, ship: Ship, saucer: Saucer) {
fun cycle(game: Game, deltaTime: Double, drawer: Drawer?) {
tick(deltaTime)
beforeInteractions()
game.stranglerCycle(deltaTime, drawer)
}
private fun beforeInteractions() = knownObjects.saucers().forEach { it.beforeInteractions() }
And, of course, I’ve removed the call to beforeInteractions
from the strangler method:
fun stranglerCycle(deltaTime: Double, drawer: Drawer?) {
processInteractions()
U.AsteroidTally = knownObjects.asteroidCount()
createNewWaveIfNeeded()
createSaucerIfNeeded()
createShipIfNeeded()
drawer?.let { draw(drawer) }
}
Test. Green. Commit: move beforeInteractions
to GameCycler.
I think that’ll do for now. Let’s sum up.
Summary
In a spirit of learning some fine tuning, we’re engaged in breaking out the game cycle from game creation in Game class, building a new GameCycler class. We’re following a “strangler” pattern where we incrementally move functionality from Game to GameCycler until there’s nothing left to move. So far, it’s going well.
I think we’ve learned one thing already, which is that we’d probably be smart to provide game to the cycler, at least until the moving is done. We would hope not to need it at all by the time we’re finished. If we do, it’ll be a sign that we may not have correctly spotted a legitimate seam between Game and GameCycler, some kind of dependency. Ideally, we’ll be able to fire up the GameCycler and use it, without it calling back to us for anything.
It shouldn’t surprise us if we have to move some things around a bit to make that possible. We might have to move them in space, from one object to another. For example, we might find it desirable to tuck something away in knownObjects
so that we can get at it. Or we might move things around in time, creating some object sooner, so as to make it available, or later, so as not to need to pass it in.
We’ll find out. The code will tell us what it wants.
For now, decent progress. GameCycler has absorbed the tick
function and the beforeInteraction
function. I fully expect that the others will be just as straightforward as it has been so far.
Join me next time and see what happens.