0

I've been playing around with akka-streams and seem to be stuck with one problem that I cannot find a clean way to handle this in a stream.

I have events coming from 1..* players with their corresponding positions on the board. I want to check if any given time players collide. For this I need to handle group of events of all currently connected players in 1 action. I could only come up with something like this which could work for 2 players and most likely groupby is not needed given events flow sequentially.

import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import akka.stream.scaladsl.{Flow, Keep, Sink, Source}
import akka.testkit.TestKit
import org.scalatest.{MustMatchers, WordSpecLike}

import scala.concurrent.Await
import scala.concurrent.duration._

case class PlayerPosition(x: Int, y: Int)
case class PlayerState(playerName: String, positions: List[PlayerPosition])

class GameLogicSpec
  extends TestKit(ActorSystem("test-filter"))
    with WordSpecLike
    with MustMatchers {


  val psa = PlayerState("A", List(PlayerPosition(0, 1)))
  val psb = PlayerState("B", List(PlayerPosition(0, 1)))

  implicit val materializer = ActorMaterializer()

  "game flow logic" must {
    "returns handles collision" in {

      val flow =
        Flow[PlayerState]
          .groupBy(2, _.playerName)
          .mergeSubstreams
          .sliding(2, 1)
          .map(evts =>
            evts.size > 1 && evts.head.positions == evts.last.positions)

      val gameLogicGraph = Source(List(psa, psb))
        .via(flow)
        .toMat(Sink.seq[Boolean])(Keep.right)

      Await
        .result(gameLogicGraph.run(), 10 seconds) must be(
        List(true)
      )
    }
  }
}

Ideally I would like the sliding window to publish events for each player evenly in distinct groups depending on the number of players currently connected. Of course the question is if 1 player is producing events faster than others would some kind of throttling would be needed, but for the sake of education I think assumption can be made they publish at the same rate.

Jeffrey Chung
  • 19,319
  • 8
  • 34
  • 54

1 Answers1

0

Assuming that there are n players, where n is equal to or greater than two (the game doesn't make sense with only one player), and that in each round, player-1 to player-n all make their respective moves before the next round begins, then using groupBy is unnecessary. In other words, if there are three players, for example, and all three players make their first move before they make their second move, and so on, then one can simply use the sliding method. Both of the arguments to that method would be equal to the number of players, and each sliding "window" would represent a round. The order in which the players moved within each round wouldn't matter (it appears that in your scenario, player-1 to player-n move in sequential order, from 1 to n, per round).

Furthermore, the function used to determine whether there is a collision in a window can be generalized for two or more players. This is done by taking the player's positions, converting this collection to a set, then checking whether the size of this set is less than the number of players: if it is, then at least two of the players hold the same position.

val numPlayers = 3

val moves = List(
  PlayerState("A", List(PlayerPosition(0, 1))), // no collision in this window
  PlayerState("B", List(PlayerPosition(1, 1))),
  PlayerState("C", List(PlayerPosition(2, 2))),

  PlayerState("A", List(PlayerPosition(0, 2))), // no collision in this window
  PlayerState("B", List(PlayerPosition(1, 2))),
  PlayerState("C", List(PlayerPosition(2, 1))),

  PlayerState("A", List(PlayerPosition(1, 2))), // collision exists in this window
  PlayerState("B", List(PlayerPosition(1, 2))),
  PlayerState("C", List(PlayerPosition(1, 2))),

  PlayerState("A", List(PlayerPosition(1, 2))), // collision exists in this window
  PlayerState("B", List(PlayerPosition(2, 2))),
  PlayerState("C", List(PlayerPosition(1, 2)))
)

implicit val materializer = ActorMaterializer()

"game flow logic" must {
  "returns handles collision" in {

    val flow =
      Flow[PlayerState]
        .sliding(numPlayers, numPlayers)
        .map(_.map(_.positions).toSet.size < numPlayers)

    val gameLogicGraph =
      Source(moves)
        .via(flow)
        .toMat(Sink.seq[Boolean])(Keep.right)

    Await.result(gameLogicGraph.run(), 10 seconds) must be(
      List(false, false, true, true)
    )
  }
}
Jeffrey Chung
  • 19,319
  • 8
  • 34
  • 54
  • hey thanks for the ideas. To give a more concrete example we can think of a snake game, in which case there could be more 1 player as well, though I think it doesn't matter from logic perspective given there won't be any collisions anyway. I'm more worried about the timing issue, eg if a window would catch 3 messages, but 2 of them would be for the same player (maybe he produces it faster). The only "stream" way to deal with it I could come up is by returning same messages back to the source (sort like a retry) though I still think there should be some ordering to get only unique messages. – Evaldas Miliauskas Mar 07 '18 at 07:14