Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic Region actor behavior #26

Merged
merged 5 commits into from
Nov 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/main/scala/com/agar/core/Agar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.agar.core.arbritrator.Arbitrator
import com.agar.core.arbritrator.Arbitrator.Start
import com.agar.core.context.{AgarContext, DefaultAgarContext}
import com.agar.core.logger.Logger
import com.agar.core.region.Region

//#main-class

Expand All @@ -16,8 +17,10 @@ object Agar extends App {
val logger: ActorRef = system.actorOf(Logger.props, "logger")
val arbitrator: ActorRef = system.actorOf(Arbitrator.props(logger),"arbitrator")

arbitrator ! Start(10000)
// A region has a size of 4 screen 1920x1080
val region = system.actorOf(Region.props(arbitrator, logger, 7680, 4320), "region")

arbitrator ! Start(10000)
}

//#main-class
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.agar.core.gameplay.energy

import com.agar.core.utils.Point2d

case class EnergyStatus(position: Point2d)
50 changes: 50 additions & 0 deletions src/main/scala/com/agar/core/gameplay/player/AreaOfInterest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.agar.core.gameplay.player

import akka.actor.ActorRef
import com.agar.core.region.{EnergyState, PlayerState}
import com.agar.core.utils.Vector2d

// ref can be the ActorRef of the virtual or the real actor
case class PlayerInfos(position: Vector2d, velocity: Vector2d, weight: Int, ref: ActorRef)

// ref can be the ActorRef of the virtual or the real actor
case class EnergyInfos(position: Vector2d, value: Int, ref: ActorRef)

case class AOI(players: List[PlayerInfos], energies: List[EnergyInfos])


object AreaOfInterest {

// There is overwhelming scientific evidence that the correct number is this one
val RADIUS_AREA_OF_INTEREST = 400

def getPlayersAOISet(players: Map[ActorRef, PlayerState], energies: Map[ActorRef, EnergyState]): Map[ActorRef, AOI] = {
players.map{ case (ref, playerState) =>
val otherPlayers = players.filter({case (actorRef, _) => isNotTheSamePlayer(actorRef, ref) })
ref -> getPlayerAOI(playerState, otherPlayers, energies)
}
}

private def getPlayerAOI(playerState: PlayerState, players: Map[ActorRef, PlayerState], energies: Map[ActorRef, EnergyState]): AOI = {
AOI(
getPlayersInAOIOfPlayer(playerState.position, players),
getEnergiesInAOIOfPlayer(playerState.position, energies)
)
}

private def getPlayersInAOIOfPlayer(playerPosition: Vector2d, players: Map[ActorRef, PlayerState]): List[PlayerInfos] =
players
.filter{case (_, p) => areClose(p.position, playerPosition)}
.map{case (r, p) => PlayerInfos(p.position, p.velocity, p.weight, r) }
.toList

private def getEnergiesInAOIOfPlayer(playerPosition: Vector2d, energies: Map[ActorRef, EnergyState]): List[EnergyInfos] =
energies
.filter{case (_, e) => areClose(e.position, playerPosition)}
.map{case (r, e) => EnergyInfos(e.position, e.value, r) }
.toList

private def isNotTheSamePlayer(a1: ActorRef, a2: ActorRef) = a1 != a2

private def areClose(p1: Vector2d, p2: Vector2d) = p1.euclideanDistance(p2) <= RADIUS_AREA_OF_INTEREST
}
88 changes: 88 additions & 0 deletions src/main/scala/com/agar/core/region/Region.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.agar.core.region


import java.util.UUID

import akka.actor.{Actor, ActorRef, Props, Stash}
import com.agar.core.context.AgarContext
import com.agar.core.gameplay.energy.Energy
import com.agar.core.gameplay.player.{AreaOfInterest, Player}
import com.agar.core.gameplay.player.Player.Init
import com.agar.core.utils.{Point2d, Vector2d}


// TODO -- move these definitions

case class PlayerState(position: Vector2d, weight: Int, velocity: Vector2d, virtual: Option[ActorRef] = Option.empty)

case class EnergyState(position: Vector2d, value: Int, virtual: Option[ActorRef] = Option.empty)

// ------------------------------

object Region {

case object GetEntitiesAOISet

final case class InitRegion(nbOfPlayer: Int, nbOfStartingEnergy: Int)

final case class Initialized(players: Map[ActorRef, PlayerState], energies: Map[ActorRef, EnergyState])

def props(arbitrator: ActorRef, logger: ActorRef, width: Int, height: Int)(implicit agarContext: AgarContext): Props = Props(new Region(arbitrator, logger)(width, height)(agarContext))
}

class Region(arbitrator: ActorRef, logger: ActorRef)(width: Int, height: Int)(implicit agarContext: AgarContext) extends Actor with Stash {

import Region._

def MAX_ENERGY_VALUE = 10
def DEFAULT_VELOCITY = Vector2d(2, 2)
def WEIGHT_AT_START = 1


def id = UUID.randomUUID()

var players: Map[ActorRef, PlayerState] = Map()
var energies: Map[ActorRef, EnergyState] = Map()

def initialized: Receive = {
case GetEntitiesAOISet => sender ! AreaOfInterest.getPlayersAOISet(this.players, this.energies)
}

def receive: Receive = {
case InitRegion(nbOfPlayer, nbOfStartingEnergy) =>
initializeEntities(nbOfPlayer, nbOfStartingEnergy)
context become initialized
logger ! Initialized(this.players, this.energies)

case _ => stash()
}

def initializeEntities(nbOfPlayer: Int, nbOfStartingEnergy: Int) = {
val r = new scala.util.Random

this.players = (0 until nbOfPlayer)
.map { n =>
val player = freshPlayer(n)
val (xStart, yStart) = (r.nextInt(width), r.nextInt(height))
player ! Init(Point2d(xStart, yStart))
player -> PlayerState(Vector2d(xStart, yStart), WEIGHT_AT_START, DEFAULT_VELOCITY)
}.toMap

this.energies = (0 until nbOfStartingEnergy)
.map { _ =>
val id = UUID.randomUUID()
val position = Vector2d(r.nextInt(width), r.nextInt(height))
val powerOfTheEnergy = 1 + r.nextInt(MAX_ENERGY_VALUE)
val energy = freshEnergy(id, powerOfTheEnergy)
energy -> EnergyState(position, powerOfTheEnergy)
}.toMap
}

private def freshPlayer(n: Int): ActorRef = {
context.actorOf(Player.props(n)(agarContext.algorithm), name = s"player-$n")
}

private def freshEnergy(id: UUID, valueOfEnergy: Int): ActorRef = {
context.actorOf(Energy.props(valueOfEnergy), name = s"energy-$id")
}
}
4 changes: 3 additions & 1 deletion src/main/scala/com/agar/core/utils/Vector2d.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.agar.core.utils

class Vector2d(val x: Double, val y: Double) {
case class Vector2d(x: Double, y: Double) {

def this() = this(0.0, 0.0)

Expand Down Expand Up @@ -49,6 +49,8 @@ class Vector2d(val x: Double, val y: Double) {
/** Returns the length of the vector |a| = sqrt((ax * ax) + (ay * ay) + (az * az)) */
def magnitude(): Double = Math.sqrt((x * x) + (y * y))

def euclideanDistance(v: Vector2d): Double = Math.sqrt(Math.pow(x - v.x, 2) + Math.pow(y - v.y, 2))

def canEqual(other: Any): Boolean =
other.isInstanceOf[Vector2d]

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.agar.core.gameplay.player

import akka.actor.{Actor, ActorSystem, Props}
import akka.testkit.TestKit
import com.agar.core.context.{AgarAlgorithm, AgarContext, AgarPosition, AgarSystem}
import com.agar.core.region.{EnergyState, PlayerState}
import com.agar.core.utils.{Point2d, Vector2d}
import org.scalatest.{Matchers, WordSpecLike}

import scala.concurrent.duration._

class AreaOfInterestSpec (_system: ActorSystem)
extends TestKit(_system)
with WordSpecLike
with Matchers {

def this() = this(ActorSystem("AgarSpec"))

"An AreaOfInterest" should {
"get the list of AOI for each players" in {
implicit val context: AgarContext = new AgarContext {
override val system: AgarSystem = () => 2 seconds
override val position: AgarPosition = () => Point2d(0, 0)
override val algorithm: AgarAlgorithm = p => Point2d(p.x + 1, p.y + 1)
}

val playerStub1 = system.actorOf(Props(new StubActor()))
val playerStub2 = system.actorOf(Props(new StubActor()))
val playerStub3 = system.actorOf(Props(new StubActor()))

val energyStub1 = system.actorOf(Props(new StubActor()))
val energyStub2 = system.actorOf(Props(new StubActor()))

val players = Map(
playerStub1 -> PlayerState(Vector2d(100, 100), 10, Vector2d(2, 2)), // close to playerStub2
playerStub2 -> PlayerState(Vector2d(300, 400), 10, Vector2d(2, 2)), // close to playerStub1
playerStub3 -> PlayerState(Vector2d(10000, 10000), 10, Vector2d(2, 2)), // alone
)

val energies = Map(
energyStub1 -> EnergyState(Vector2d(200, 200), 10), // energy for playerStub1, playerStub2
energyStub2 -> EnergyState(Vector2d(9800, 9800), 10), // energy for playerStub3
)

val areaOfInterestSet = AreaOfInterest.getPlayersAOISet(players, energies)

areaOfInterestSet.get(playerStub1) should be (
Some(
AOI(
List(PlayerInfos(Vector2d(300, 400),Vector2d(2, 2), 10, playerStub2)),
List(EnergyInfos(Vector2d(200, 200), 10, energyStub1))
)
)
)

areaOfInterestSet.get(playerStub2) should be (
Some(
AOI(
List(PlayerInfos(Vector2d(100, 100), Vector2d(2, 2), 10, playerStub1)),
List(EnergyInfos(Vector2d(200, 200), 10, energyStub1))
)
)
)

areaOfInterestSet.get(playerStub3) should be (
Some(
AOI(
List.empty,
List(EnergyInfos(Vector2d(9800, 9800), 10, energyStub2))
)
)
)
}
}

class StubActor() extends Actor {
override def receive: Receive = { case e => sender ! e }
}
}
56 changes: 56 additions & 0 deletions src/test/scala/com/agar/core/region/RegionTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.agar.core.region

import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import akka.testkit.{TestKit, TestProbe}
import com.agar.core.context.{AgarAlgorithm, AgarContext, AgarPosition, AgarSystem}
import com.agar.core.region.Region.{InitRegion, Initialized}
import com.agar.core.utils.Point2d
import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}

import scala.concurrent.duration._

class RegionTest (_system: ActorSystem)
extends TestKit(_system)
with Matchers
with WordSpecLike
with BeforeAndAfterAll {

def this() = this(ActorSystem("AgarSpec"))

override def afterAll: Unit = {
shutdown(system)
}

"A Region Actor" should {

"initialize a fresh region with fresh entities" in {
implicit val context: AgarContext = new AgarContext {
override val system: AgarSystem = () => 2 seconds
override val position: AgarPosition = () => Point2d(0, 0)
override val algorithm: AgarAlgorithm = p => Point2d(p.x + 1, p.y + 1)
}

val testProbe = TestProbe()
val tracer = system.actorOf(Props(new Tracer(testProbe.ref)))

val region = system.actorOf(Region.props(ActorRef.noSender, tracer, 7680, 4320), "region")

region ! InitRegion(2, 2)

val expectedCorrectInit = testProbe.expectMsgPF() {
case Initialized(players, energies) => {
players.size === 2 && energies.size === 2
}
}

expectedCorrectInit should be (true)
}
}

class Tracer(a: ActorRef) extends Actor {
override def receive: Receive = {
case e =>
a ! e
}
}
}