5 minutes
CR: Truly Object-oriented in ‘Twin Peaks’
This is the first post in the CR–Code Review–series. I hit upon the idea after I found this interesting discussion regarding Swift playgrounds, book 2 (Let’s Code 2), chapter named ‘Twin Peaks’ in Scrabtree’s website, where he lamented his very long solution compared to Samantha’s shorter solution.
The purpose of this CR is to bring learners’ attention to an interesting thing about object orientation: objects can be interdependent even if they were instantiated independently. In doing so, I also want to present a new and interesting solution to the ‘Twin Peaks’ challenge, which solution totally bypasses the expected assumptions learners might have about this problem; that is, my motivation is to show that it is okay to devise solutions that may not toe the instruction line.
In this challenge, you are supposed to create an Expert() object, and a Character() object, where the expert’s task is to tinker with the lock that controls the adjustable platform in the middle between the the twin peaks. Then the character is supposed to jump and move, collecting gems as they randomly appear. By the way, you are given the ability to place them whereever you like.
If the character reaches the end of the land (hits a block) it has to turn around. If we don’t create such a turn-around rule, then character cannot turn around and continue moving and picking gems when it hits blockades at the outer edges of the land.
But, if we devise a turn-around rule for a blockade, the character would turn around on each step of the peaks. This can be overcome with the character.jump() method, but if we apply this rule, then it would not turn around when reaching the outer-edge blockades but simply keep on jumping.
Besides, you must also ensure the character can turn left or right according to specific conditions (such as when it is blocked, but not blocked on the left). The problem here is, if you devise a general rule for left or right turns, such a condition would also apply midway on peak steps, causing character to turn when he should be going straight to collect gems.
That occurs because the platform has been raised enough to allow character to turn left or right into the peaks. A tentative solution might have been to instruct expert to change the platform level so that the character does not turn left or right when he should not, but this would make the code longer than it should be, and also would create its own new problems.
Scrabtree’s solution: (I have renamed character and expert as ch and ex respectively, to shorten the width):
let totalGems = RandomNumberofGems
let ex = Expert()
let ch = Character()
var gemCount = 0
world.place(ex, facing: north, atColumn:0, row:3)
world.place(ch, facing: north, atColumn:4, row:0)
func rowRun() {
for i in 1...6 {
if ch.isOnGem {
gemCount += 1
ch.collectGem()
}
ch.jump()
}
}
func turnLock(up:Bool, turns: Int) {
if up == true {
for i in 1...turns {
ex.turnLockUp()
}
} else {
for i in 1...turns {
ex.turnLockDown()
}
}
ex.turnRight()
}
func leftFace() {
ch.turnLeft()
ch.moveForward()
ch.turnLeft()
}
func rightFace() {
ch.turnRight()
ch.moveForward()
ch.turnRight()
}
ex.moveForward()
rowRun()
turnLock(up:true, turns:3)
leftFace()
rowRun()
rightFace()
rowRun()
turnLock(up:false, turns:3)
rightFace()
rowRun()
leftFace()
By the way, his solution does not solve the puzzle:
He cites Samantha’s solution as better because it is 31 lines. Indeed, it is quite succinct:
let totalGems = RandomNumberofGems
let ex = Expert()
let ch = Character()
var gemCount = 0
func moveLong() {
for i in 1...6 {
if ch.isOnGem {
ch.collectGem()
gemCount += 1
}
ch.jump()
}
}
func moveShort() {
for i in 1...2 {
if ch.isOnGem {
ch.collectGem()
gemCount += 1
}
ch.jump()
}
}
world.place (ex, facing: north, atColumn:1, row:4)
world.place (ch, facing: north, atColumn:3, row:0)
ex.turnLock(up:true, numberOfTimes:2)
while gemCount < totalGems {
moveLong()
ch.turnRight()
moveShort()
ch.turnRight()
}
Samantha’s solution works everytime. In the above demo, I ran it twice. In the second run, you notice it took a considerably longer time to finish the task because this one character was going round and round. In other words, the object was overworked.
What I wanted to contribute to this discussion is the idea it is not necessary to make your objects work harder! Just create more objects. And then, you can also use the actions of one object to be a trigger for the actions of other objects. Here is my solution which is even shorter (23 lines) and did not even make use of functions, but I believe it is much clearer because I created three characters, made good use of OR and did away with the so-called expert. Here is a demonstration which also takes less time (one-third because now there are three objects, and also there is no time wasted on the expert) to finish the task:
First, I created three characters and named each ch, gh, and ph. Then I instructed them to do their job along their paths, but in order to circumvent the turn issue, I instructed ch, who was born on the platform to be the trigger. As soon as ch hits the edge block, it turns around, but so do the other two then. Because ch moves one step faster than the other two, sometimes this might cause a one-square lag in gh and ph, but eventually they catch up and turn, too. I ran the simulation 40 times and the solution worked each time:
let totalGems = RandomNumberofGems
let ch = Character(), gh = Character(), ph = Character()
world.place(ch, facing: .north, atColumn: 3, row: 0 )
world.place(gh, facing: .north, atColumn: 2, row: 0 )
world.place(ph, facing: .north, atColumn: 4, row: 0 )
var gems = 0
while gems < totalGems{
if ch.isOnGem || gh.isOnGem || ph.isOnGem {
ch.collectGem() || gh.collectGem() || ph.collectGem()
gems += 1
}
ch.moveForward()
gh.jump()
ph.jump()
if ch.isBlocked{
ch.turnLeft()
ch.turnLeft()
gh.turnLeft()
gh.turnLeft()
ph.turnLeft()
ph.turnLeft()
}
}
As you can see, in my solution, it is easy to reason about the program. It make sense in one glance.
The key point is: code should be highly readable and short at the same time. Another key point: more objects doing simpler things is better than a few objects doing too many things. Another point: use one object to trigger actions in other objects.