-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add back and forth game skeleton
- Loading branch information
Showing
20 changed files
with
444 additions
and
33 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
8 changes: 8 additions & 0 deletions
8
.idea/modules/back-and-forth/PPS-22-direct-style-experiments.back-and-forth.main.iml
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
8 changes: 8 additions & 0 deletions
8
.idea/modules/back-and-forth/PPS-22-direct-style-experiments.back-and-forth.test.iml
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
24 changes: 24 additions & 0 deletions
24
back-and-forth/src/main/scala/io/github/tassiLuca/Launcher.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package io.github.tassiLuca | ||
|
||
import gears.async.default.given | ||
import gears.async.Async | ||
|
||
import concurrent.duration.DurationInt | ||
import io.github.tassiLuca.boundary.impl.{SwingUI, Timer} | ||
import io.github.tassiLuca.core.{Controller, Space2D} | ||
|
||
import scala.language.postfixOps | ||
|
||
@main def main(): Unit = Async.blocking: | ||
val view = SwingUI(500, 800) | ||
val timer = Timer(1 seconds) | ||
view.asRunnable.run | ||
timer.asRunnable.run | ||
val controller = Controller | ||
.reactive( | ||
boundaries = Set(timer), | ||
updatableBoundaries = Set(view), | ||
reaction = (_, s) => s, | ||
)(Space2D((450, 700))) | ||
.run | ||
controller.await |
11 changes: 11 additions & 0 deletions
11
back-and-forth/src/main/scala/io/github/tassiLuca/boundary/Boundary.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package io.github.tassiLuca.boundary | ||
|
||
import gears.async.{Async, Task} | ||
import io.github.tassiLuca.core.Event | ||
|
||
trait Boundary: | ||
def src: Async.Source[Event] | ||
def asRunnable: Task[Unit] | ||
|
||
trait UpdatableBoundary[M] extends Boundary: | ||
def update(model: M): Unit |
18 changes: 18 additions & 0 deletions
18
back-and-forth/src/main/scala/io/github/tassiLuca/boundary/BoundarySource.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package io.github.tassiLuca.boundary | ||
|
||
import gears.async.{Async, Listener} | ||
import io.github.tassiLuca.core.Event | ||
|
||
object BoundarySource extends Async.OriginalSource[Event]: | ||
private[boundary] var listeners = Set[Listener[Event]]() | ||
|
||
override def poll(k: Listener[Event]): Boolean = false | ||
|
||
override def dropListener(k: Listener[Event]): Unit = synchronized: | ||
listeners = listeners - k | ||
|
||
override protected def addListener(k: Listener[Event]): Unit = synchronized: | ||
listeners = listeners + k | ||
|
||
def notifyListeners(e: Event): Unit = synchronized: | ||
listeners.foreach(_.completeNow(e, this)) |
50 changes: 50 additions & 0 deletions
50
back-and-forth/src/main/scala/io/github/tassiLuca/boundary/impl/SwingUI.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package io.github.tassiLuca.boundary.impl | ||
|
||
import gears.async.{Async, Task} | ||
import io.github.tassiLuca.boundary.{BoundarySource, UpdatableBoundary} | ||
import io.github.tassiLuca.core.Event.{ChangeDirection, Freeze} | ||
import io.github.tassiLuca.core.{Event, RectangularEntities, Space2D} | ||
|
||
import java.awt.event.{KeyAdapter, KeyEvent} | ||
import java.awt.{BorderLayout, Color, Graphics} | ||
import javax.swing.border.LineBorder | ||
import javax.swing.{JFrame, JPanel, SwingUtilities, WindowConstants} | ||
|
||
class SwingUI(width: Int, height: Int) extends UpdatableBoundary[Space2D & RectangularEntities]: | ||
private val boundarySource = BoundarySource | ||
private val frame = JFrame("Popping Bubbles game") | ||
|
||
frame.addKeyListener( | ||
new KeyAdapter: | ||
override def keyPressed(e: KeyEvent): Unit = e.getKeyCode match | ||
case 38 | 40 => boundarySource.notifyListeners(ChangeDirection) | ||
case 32 => boundarySource.notifyListeners(Freeze) | ||
case _ => (), | ||
) | ||
|
||
override inline final def src: Async.Source[Event] = boundarySource | ||
|
||
override def asRunnable: Task[Unit] = Task { | ||
frame.setSize(width, height) | ||
frame.setVisible(true) | ||
frame.setLocationRelativeTo(null) | ||
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE) | ||
} | ||
|
||
override def update(space: Space2D & RectangularEntities): Unit = SwingUtilities.invokeAndWait { () => | ||
if frame.getContentPane.getComponentCount != 0 then frame.getContentPane.remove(0) | ||
frame.getContentPane.add(WorldPane(space, space.world.shape.width, space.world.shape.height), BorderLayout.CENTER) | ||
frame.getContentPane.repaint() | ||
} | ||
|
||
private class WorldPane(space: Space2D & RectangularEntities, width: Int, height: Int) extends JPanel: | ||
setSize(width, height) | ||
setBorder(LineBorder(Color.DARK_GRAY, 2)) | ||
|
||
override def paintComponent(g: Graphics): Unit = | ||
val (px, py) = space.world.player.position | ||
g.fillRect(px, py, space.world.player.shape.width, space.world.player.shape.height) | ||
space.world.obstacles.foreach(o => | ||
val (px, py) = o.position | ||
g.drawRect(px, py, o.shape.width, o.shape.height), | ||
) |
16 changes: 16 additions & 0 deletions
16
back-and-forth/src/main/scala/io/github/tassiLuca/boundary/impl/Timer.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package io.github.tassiLuca.boundary.impl | ||
|
||
import gears.async.{Async, Task} | ||
import gears.async.default.given | ||
import io.github.tassiLuca.boundary.Boundary | ||
import io.github.tassiLuca.core.Event | ||
|
||
import scala.concurrent.duration.Duration | ||
|
||
class Timer(period: Duration) extends Boundary: | ||
private val timer = gears.async.Timer(period) | ||
|
||
override def src: Async.Source[Event] = | ||
timer.src.transformValuesWith(_ => Event.Tick) | ||
|
||
override def asRunnable: Task[Unit] = Task(timer.run()) |
44 changes: 44 additions & 0 deletions
44
back-and-forth/src/main/scala/io/github/tassiLuca/core/Controller.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package io.github.tassiLuca.core | ||
|
||
import gears.async.TaskSchedule.RepeatUntilFailure | ||
import gears.async.{Async, Future, ReadableChannel, SendableChannel, Task, UnboundedChannel} | ||
import io.github.tassiLuca.boundary.{Boundary, UpdatableBoundary} | ||
|
||
import java.time.LocalTime | ||
import scala.annotation.tailrec | ||
|
||
trait Controller[Event, Model]: | ||
def reactive( | ||
boundaries: Set[Boundary], | ||
updatableBoundaries: Set[UpdatableBoundary[Model]], | ||
reaction: Reaction[Event, Model], | ||
)(initial: Space2D & RectangularEntities): Task[Unit] | ||
|
||
object Controller extends Controller[Event, Space2D & RectangularEntities]: | ||
override def reactive( | ||
boundaries: Set[Boundary], | ||
updatableBoundaries: Set[UpdatableBoundary[Space2D & RectangularEntities]], | ||
reaction: Reaction[Event, Space2D & RectangularEntities], | ||
)(initial: Space2D & RectangularEntities): Task[Unit] = Task: | ||
val channel = UnboundedChannel[Event]() // may go out of memory! | ||
val producers = (boundaries ++ updatableBoundaries).map(_ produceOn channel) | ||
Future { consumeFrom(updatableBoundaries, channel, initial, reaction) } | ||
producers.map(_.run).toSeq.awaitAll // important! | ||
|
||
extension (b: Boundary) | ||
private def produceOn(channel: SendableChannel[Event]): Task[Unit] = Task { | ||
channel.send(b.src.awaitResult) | ||
}.schedule(RepeatUntilFailure()) | ||
|
||
@tailrec | ||
private def consumeFrom( | ||
updatableBoundaries: Set[UpdatableBoundary[Space2D & RectangularEntities]], | ||
channel: ReadableChannel[Event], | ||
space2D: Space2D & RectangularEntities, | ||
reaction: Reaction[Event, Space2D & RectangularEntities], | ||
)(using Async): Unit = | ||
val event = channel.read().toOption.get // may fail due to closed channel | ||
println(s"Consuming $event @ ${LocalTime.now()}") | ||
val newSpace = reaction(event, space2D) | ||
updatableBoundaries.foreach(_.update(newSpace)) | ||
consumeFrom(updatableBoundaries, channel, space2D, reaction) |
6 changes: 6 additions & 0 deletions
6
back-and-forth/src/main/scala/io/github/tassiLuca/core/Event.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package io.github.tassiLuca.core | ||
|
||
enum Event: | ||
case Tick | ||
case ChangeDirection | ||
case Freeze |
9 changes: 9 additions & 0 deletions
9
back-and-forth/src/main/scala/io/github/tassiLuca/core/Reaction.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package io.github.tassiLuca.core | ||
|
||
import scala.annotation.targetName | ||
|
||
type Reaction[Event, Model] = (Event, Model) => Model | ||
|
||
extension [A, B, C](f: (A, B) => C) | ||
@targetName("chainWith") | ||
def >>[D](g: (A, C) => D): (A, B) => D = (a, b) => g(a, f(a, b)) |
53 changes: 53 additions & 0 deletions
53
back-and-forth/src/main/scala/io/github/tassiLuca/core/Space.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package io.github.tassiLuca.core | ||
|
||
import io.github.tassiLuca.utils.* | ||
|
||
/** The N-d space on which the game world is absorbed. */ | ||
trait Space: | ||
|
||
type Position | ||
type Speed | ||
type Shape | ||
|
||
def world: World | ||
def world_=(world: World): Unit | ||
|
||
sealed trait Entity: | ||
val position: Position | ||
val speed: Speed | ||
val shape: Shape | ||
|
||
class Player(override val position: Position, override val speed: Speed, override val shape: Shape) extends Entity | ||
class Obstacle(override val position: Position, override val speed: Speed, override val shape: Shape) extends Entity | ||
|
||
case class World(player: Player, obstacles: Set[Obstacle], shape: Shape) | ||
|
||
extension (e: Entity) def isCollidingWith(e2: Entity): Boolean | ||
|
||
/** A 2 dimensional space. */ | ||
trait Space2D extends Space: | ||
override type Speed = Vector2D | ||
override type Position = Point2D | ||
|
||
/** A mixin specifying rectangular shapes along with the definition of collision among them. */ | ||
trait RectangularEntities: | ||
self: Space2D => | ||
|
||
case class RectangularShape(width: Int, height: Int) | ||
|
||
override type Shape = RectangularShape | ||
|
||
extension (e: Entity) | ||
override def isCollidingWith(e2: Entity): Boolean = | ||
(e.position._1 <= e2.position._1 + e2.shape.width) && (e.position._1 + e.shape.width >= e2.position._1) && | ||
(e.position._2 <= e2.position._2 + e2.shape.height) && (e.position._2 + e.shape.height >= e2.position._2) | ||
|
||
object Space2D: | ||
def apply(size: Point2D): Space2D with RectangularEntities = new Space2D with RectangularEntities: | ||
var _world: World = World( | ||
player = Player(size / 2, (0, 1.0), RectangularShape(size._1 / 10, size._2 / 10)), | ||
obstacles = Set.empty, | ||
shape = RectangularShape(size._1, size._2), | ||
) | ||
override def world: World = _world | ||
override def world_=(world: World): Unit = _world = world |
17 changes: 17 additions & 0 deletions
17
back-and-forth/src/main/scala/io/github/tassiLuca/utils/Types2D.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package io.github.tassiLuca.utils | ||
|
||
import scala.annotation.targetName | ||
|
||
type Point2D = (Int, Int) | ||
|
||
type Vector2D = (Double, Double) | ||
|
||
extension (p: Point2D) | ||
|
||
@targetName("divideBy") | ||
def /(i: Int): Point2D = (p._1 / i, p._2 / i) | ||
|
||
@targetName("subtract") | ||
def -(i: Int): Point2D = (p._1 - i, p._2 - i) | ||
|
||
extension (v: Vector2D) def module: Double = Math.sqrt(v._1 * v._1 + v._2 * v._2) |
33 changes: 33 additions & 0 deletions
33
back-and-forth/src/test/scala/io/github/tassiLuca/boundary/impl/TimerTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package io.github.tassiLuca.boundary.impl | ||
|
||
import gears.async.default.given | ||
import gears.async.{Async, Listener} | ||
import io.github.tassiLuca.core.Event | ||
import org.scalatest.flatspec.AnyFlatSpec | ||
import org.scalatest.matchers.should.Matchers | ||
|
||
import concurrent.duration.DurationInt | ||
import scala.language.postfixOps | ||
|
||
class TimerTest extends AnyFlatSpec with Matchers: | ||
|
||
"Timer when run" should "generate Tick events observable waiting for them" in { | ||
val timer = Timer(1 seconds) | ||
val before = System.currentTimeMillis() | ||
Async.blocking: | ||
timer.asRunnable.run | ||
timer.src.awaitResult shouldBe Event.Tick | ||
val now = System.currentTimeMillis() | ||
(now - before) should (be > 1_000L) | ||
} | ||
|
||
"Timer when run" should "generate Tick events observable attaching a listener" in { | ||
Async.blocking: | ||
val timerPeriod = 1 seconds | ||
val timer = Timer(timerPeriod) | ||
var event: Event = null | ||
timer.src.onComplete(Listener((e, _) => event = e)) | ||
timer.asRunnable.run | ||
Thread.sleep(timerPeriod.toMillis + 100) | ||
event shouldBe Event.Tick | ||
} |
35 changes: 35 additions & 0 deletions
35
back-and-forth/src/test/scala/io/github/tassiLuca/core/SpaceTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package io.github.tassiLuca.core | ||
|
||
import org.scalatest.flatspec.AnyFlatSpec | ||
import org.scalatest.matchers.should.Matchers | ||
|
||
class SpaceTest extends AnyFlatSpec with Matchers: | ||
|
||
"Creating a 2D space" should "initialize a world of the given size with just a player and no obstacles" in { | ||
val size = (100, 150) | ||
val space = Space2D(size) | ||
space.world.player.position should be < size | ||
space.world.obstacles.size shouldBe 0 | ||
space.world.shape.width shouldBe size._1 | ||
space.world.shape.height shouldBe size._2 | ||
} | ||
|
||
"It" should "be possible to update the world" in {} | ||
|
||
"Checking collision between two 2D rectangular shapes" should "work" in { | ||
val space = Space2D((10, 10)) | ||
space.world.player.position shouldBe (5, 5) | ||
space.world.player.shape shouldBe space.RectangularShape(1, 1) | ||
val collidingObstacles = Set( | ||
space.Obstacle((4, 3), (0, 0), space.RectangularShape(1, 2)), | ||
space.Obstacle((5, 6), (0, 0), space.RectangularShape(1, 2)), | ||
space.Obstacle((6, 5), (0, 0), space.RectangularShape(2, 1)), | ||
) | ||
val nonCollidingObstacles = Set( | ||
space.Obstacle((0, 0), (0, 0), space.RectangularShape(1, 2)), | ||
space.Obstacle((7, 6), (0, 0), space.RectangularShape(3, 3)), | ||
) | ||
space.world = space.World(space.world.player, collidingObstacles ++ nonCollidingObstacles, space.world.shape) | ||
collidingObstacles.foreach(o => space.isCollidingWith(space.world.player)(o) shouldBe true) | ||
nonCollidingObstacles.foreach(o => space.isCollidingWith(space.world.player)(o) shouldBe false) | ||
} |
Oops, something went wrong.