OOP Robots Fight
This is a longer exercise created to practice knowledge from OOP intro and _FP intro _workshops. It will have 5 parts and each part is independent in a sense that it is not using any code from previous parts - yet still it is a development of the main exercise.
PART1
FILE : jug.lodz.workshops.starter.capstone.robots.Robots1
class Robot1(initialEnergy:Int){
require(???,s"initial energy must be larger than 0, actual $initialEnergy")
def isAlive : Boolean = ???
def takeHit(damage:Int):Unit = ???
def energyStatus:Int = ???
}
here you need to fill all 4 places with '???' signs. Use tests defined below the main file as a specification.
PART2
FILE : jug.lodz.workshops.starter.capstone.robots.Robots2
Add method punch to Robot. This method should generate an Int from a range [0,50] which simbolizes hit strength (0 is miss).
class Robot2 private(initialEnergy:Int){
(...)
def punch : Int = ???
}
Also train creating companion objects and factory methods. Notice that Robot2 constructor is private so only companion object has access to it. Also you may notice named argyment withForceField which doubles robot energy. Use tests below code as a specification
object Robot2{
private val forceFieldMultiplier = 2
def apply(initialEnergy:Int, withForceField:Boolean=false):Robot2 = ???
def battle(r1:Robot2,r2:Robot2): Unit = ???
}
PART3
FILE : jug.lodz.workshops.starter.capstone.robots.Robots3
Testing random values is not very convenient so we are going to encapsulate hit generation logic behind interface which can be mocked. So in the following you need to add two implementations
- Constant hit generator - good for testing
- Random hit generator - for production
trait PunchGenerator{
def generatePunch():Int
}
object PunchGenerator{
def const(value:Int):PunchGenerator = ???
def random(upTo:Int):PunchGenerator = ???
}
Also we can extract shield logic
trait Shield{
def absorb(hit:Int):Int
}
object Shield{
def createShield(energy:Int):Shield = ???
def noShield():Shield= ???
}
absorb method receive original hit and returns amount of energy not absorbed by it , so in test we have
test("No shield is not protecting at all"){
Robots3.Shield.noShield().absorb(10) mustBe 10
Robots3.Shield.noShield().absorb(10) mustBe 10
Robots3.Shield.noShield().absorb(10) mustBe 10
}
test("Standard shield should absorb hits"){
val shield=Robots3.Shield.createShield(100)
shield.absorb(40) mustBe 0
shield.absorb(40) mustBe 0
shield.absorb(40) mustBe 20
shield.absorb(40) mustBe 40
}
noShield doesn't absorb energy so it always returns whatever it received. On the other hand if we have normal shield with energy 100 then it was able to fully absorb two first 40 punches, then half of third punch (40+40+20=100)
and now you can easily inject your mocks into Robot
class Robot3 private(initialEnergy:Int,shield: Shield,punchGenerator: PunchGenerator){
PART4
FILE : jug.lodz.workshops.starter.capstone.robots.Robots4
This time we will recall knowledge about function syntax. So this time instead of just method we will work with fields of a function type or with methods returning functions.
So for example in following code factory methods are now just object fields of function type.
trait PunchGenerator{
def generatePunch: () => Int
}
object PunchGenerator{
private class ConstantPunchGenerator(hit:Int) extends PunchGenerator {
override def generatePunch : () => Int = ???
}
private class RandomPuchGenerator(upTo:Int) extends PunchGenerator{
override def generatePunch: () => Int = ???
}
val const : Int => PunchGenerator = ???
val random : Int => PunchGenerator = ???
}
Notice that in Robot class we are now using function composition. This way we are able to separate part of energy absorbtion by shield from calculating robot energy after hit.
class Robot4 private(initialEnergy:Int,shield: Shield,punchGenerator: PunchGenerator){
private var energy:Int=initialEnergy
val isAlive : () => Boolean = ???
val energyStatus:() => Int = ???
private val absorb : Int => Int = ???
private val reduceEnergy : Int =>Unit = ???
val takeHit : Int => Unit = absorb andThen reduceEnergy
def punch : Int = punchGenerator.generatePunch()
}
Our functions have strange types like Unit or () . This is because we are depending on external state - in further workshops we will learn the difference between such functions and a pure functions which doesn't depend on a context.
PART5
FILE : jug.lodz.workshops.starter.capstone.robots.Robots5
Here you need to use partial functions. Let's start from absorbing hit function
private val shieldBroken: PartialFunction[Int, Int] = ???
private val absorbHit: PartialFunction[Int, Int] = ???
override def absorb: PartialFunction[Int, Int] = absorbHit orElse shieldBroken
While normal function compose horizontally - partial functions can be compose vertically - in other words program will try to match one function after another. If there is no match then an exception is thrown.
Finally finish Game object
trait GameState
object Continue extends GameState
object End extends GameState
object Game{
val battle: (Robot5,Robot5) => Unit = (r1,r2) => r2.takeHit(r1.punch)
val gameState:PartialFunction[Robot5,GameState] = ???
}
object GameMessagesGenerator {
val generateMessage:PartialFunction[GameState,String] = ???
}
That's all for today. In next chapters you will learn how to design programs so you can write pure functions more naturally.