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.

results matching ""

    No results matching ""