Skip to content
This repository has been archived by the owner on Sep 28, 2023. It is now read-only.

Commit

Permalink
Release version 1.0.0
Browse files Browse the repository at this point in the history
*create the ground
*create the missile
*check collisions
*restart the game

Co-authored-by: Matteo Lazzari <[email protected]>
Co-authored-by: Daniele Di Lillo <[email protected]>
  • Loading branch information
3 people authored Nov 15, 2022
1 parent e450e4e commit ed7bdb2
Show file tree
Hide file tree
Showing 100 changed files with 4,762 additions and 14 deletions.
46 changes: 37 additions & 9 deletions .github/workflows/scala.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,46 @@ name: Scala CI
on:
push:

permissions:
contents: write
checks: write

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: 'sbt'
- name: Run tests
run: sbt test
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: 'sbt'
- run: sbt clean
- name: Run tests
run: sbt coverage test
- name: Generate coverage report
run: sbt coverageReport
# - name: Generate documentation
# run: sbt doc
- name: Report
uses: dorny/test-reporter@v1
if: always()
with:
name: Scala Tests
path: target/test-reports/*.xml
reporter: java-junit
fail-on-error: true
- name: Upload coverage to GitHubPage
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: target/scala-3.2.0/scoverage-report # The folder the action should deploy.
branch: coverage-deploy # The branch the action should deploy to.
# - name: Upload documentation
# uses: actions/upload-artifact@v3
# with:
# name: documentation
# path: target/scala-3.2.0/api/

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ project/plugins/project/
.history
.cache
.lib/
.idea/
/.bsp/*

# End of https://www.toptal.com/developers/gitignore/api/intellij+all,sbt
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
# PPS-22-mis-com
# PPS-22-mis-com
# Missile Command
- - -
## pps-2022-missile-command
authors: Andrea Brighi, Daniele Di Lillo, Matteo Lazzari
## Introduction
This project is a reimplementation of the classic game Missile Command, developed Scala.
The project is developed using a functional programming paradigm and a TDD approach.
22 changes: 19 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / version := "1.0.0"

ThisBuild / scalaVersion := "3.2.0"

val scalatest = "org.scalatest" %% "scalatest" % "3.2.12" % Test
val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.17.0" % "test"
val scalactic = "org.scalactic" %% "scalactic" % "3.2.14"

ThisBuild / scalaVersion := "3.1.3"

lazy val root = (project in file("."))
.settings(
name := "missile command"
name := "missile command",
libraryDependencies ++= Seq(scalatest, scalaCheck, scalactic)
++
Seq(
"io.monix" %% "monix" % "3.4.1",
"dev.optics" %% "monocle-core" % "3.1.0",
"dev.optics" %% "monocle-macro" % "3.1.0"
),
assembly / mainClass := Some("view.startGame"),
assembly / assemblyJarName := "missile_command.jar",
)


2 changes: 2 additions & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.3")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.0.0")
Binary file added src/main/resources/Base_false_0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/Base_false_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/Base_false_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/Base_false_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/Base_true_0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/Base_true_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/Base_true_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/Base_true_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/city_0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/city_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/city_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/city_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/enemy_missile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/friendly_missile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/satellite.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions src/main/scala/controller/Event.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package controller

import model.elements2d.Point2D

/**
* Enum of the possible events in the game.
*/
enum Event:
/**
* Class that represents the event of passing time.
*
* @param time the time passed
*/
case TimePassed(deltaTime: Double)

/**
* Class that represents the event of launching a friendly missile.
*
* @param position the ending position of the missile
*/
case LaunchMissileTo(position: Point2D)
57 changes: 57 additions & 0 deletions src/main/scala/controller/GameLoop.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package controller

import monix.eval.Task
import monix.execution.Ack
import monix.execution.Scheduler.Implicits.global
import monix.reactive.subjects.PublishSubject
import monix.reactive.{Observable, Observer, OverflowStrategy}
import org.reactivestreams.Subscriber
import view.gui.UI
import model.World
import controller.update._

import scala.concurrent.Future
import scala.concurrent.duration.DurationInt
import scala.language.postfixOps
import scala.util.Random

/**
* Object that represents the controller of the game.
*/
object GameLoop:
/**
* The observable that will be used to schedule time in the game.
*/
private val time: Observable[Event] = TimeFlow
.tickEach(50 milliseconds)
.map(_.toDouble/1000)
.map(Event.TimePassed.apply)

/**
* start the game loop.
*
* @param ui the ui that will be used to show the game.
* @return a task that will execute the game.
*/
def start(ui: UI): Task[Unit] =

given OverflowStrategy[Event] = OverflowStrategy.Default

val world = World.initialWorld
val controls: Update = Update.combine(
UpdateTime(),
UpdatePosition(),
ActivateSpecialAbility(),
CollisionsDetection(),
LaunchNewMissile()
)

val init = Task((world, controls))
val events =
Observable(time, ui.events.throttleLast(50 milliseconds)).merge
events
.scanEval(init) { case ((world, controls), event) => controls(event, world) }
.doOnNext { case (world, _) => ui.render(world) }
.takeWhile { case (world, _) => world.ground.stillAlive }
.completedL
.doOnFinish(_ => ui.gameOver)
20 changes: 20 additions & 0 deletions src/main/scala/controller/TimeFlow.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package controller

import monix.reactive.Observable
import scala.concurrent.duration.FiniteDuration

/**
* A object that con be used to create a [[monix.reactive.Observable]] that emits a value every time a given time interval has passed.
*/
object TimeFlow:
/**
* Creates a [[monix.reactive.Observable]] that emits a value every time a given time interval has passed.
*
* @param duration the time interval between two consecutive values
* @return an [[monix.reactive.Observable]] that emits a value every time a given time interval has passed
*/
def tickEach(duration: FiniteDuration): Observable[Long] =
Observable
.fromIterable(LazyList.continually(duration))
.delayOnNext(duration)
.map(_.toMillis)
37 changes: 37 additions & 0 deletions src/main/scala/controller/update/ActivateSpecialAbility.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package controller.update

import controller.Event
import controller.Event.TimePassed
import controller.update.Update.on
import model.World
import model.explosion.Explosion
import model.behavior.Moveable
import model.collisions.Collisionable
import model.missile.Missile
import monix.eval.Task

/**
* Object that return an update function for the world to be update with the special abilities of its components
*/
object ActivateSpecialAbility:
/**
* Apply function used to update the world to be update with the special abilities of its components
*
* @return An Update that update the world to be update with the special abilities of its components
*/
def apply(): Update = on[TimePassed] { (_: Event, world: World) =>
Task {
def activateSpecialAbility(collisionable: Collisionable): Collisionable = collisionable match
case missile: Missile if missile.destinationReached => missile.explode
case _ => collisionable

def isTerminated(collisionable: Collisionable): Boolean = collisionable match
case explosion: Explosion => explosion.terminated
case _ => false

val collisionables = world.collisionables.map(activateSpecialAbility)
val remainedCollisionables = collisionables.filterNot(isTerminated)
val (newMissiles, spawner) = world.spawner.spawn()
world.copy(collisionables = remainedCollisionables ++ newMissiles, spawner = spawner)
}
}
49 changes: 49 additions & 0 deletions src/main/scala/controller/update/CollisionsDetection.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package controller.update

import controller.Event
import controller.Event.TimePassed
import controller.update.Update.on
import model.{World, calculateNewScore}
import model.collisions.{Collisionable, Damageable, applyDamage, calculateCollisions}
import model.ground.{City, Ground, MissileBattery}
import model.missile.Missile
import monix.eval.Task

/**
* Object that return an update function for the world to be update with the aftermath of its components collisions
*/
object CollisionsDetection:
/**
* Apply function used to update the world to be update with the aftermath of its components collisions
*
* @return An Update that update the world to be update with the aftermath of its components collisions
*/
def apply(): Update = on[TimePassed] { (_: Event, world: World) =>
Task {
def createNewGround(collisionables: List[Collisionable]): (Ground, List[Collisionable]) =
val (collisions, groundElements) = collisionables.partition(_ match
case _: MissileBattery => false
case _: City => false
case _ => true
)
val (missileBatteries, cities) = groundElements.partition(_ match
case _: MissileBattery => true
case _: City => false
)
(Ground(
cities.map(_.asInstanceOf[City]),
missileBatteries.map(_.asInstanceOf[MissileBattery])
), collisions)

def isDestroyed(collisionable: Collisionable): Boolean = collisionable match
case damageable: Damageable => damageable.isDestroyed
case _ => false

val collisionables = world.collisionables ++ world.ground.cities ++ world.ground.turrets
val collisionableAfterCollisions = applyDamage(calculateCollisions(collisionables))
val newScore = calculateNewScore(collisionableAfterCollisions, world.score)
val (newGround, newCollisionables) = createNewGround(collisionableAfterCollisions.keys.toList)
val newNotDestroyedCollisionables = newCollisionables.filterNot(isDestroyed)
world.copy(collisionables = newNotDestroyedCollisionables, score = newScore, ground = newGround)
}
}
28 changes: 28 additions & 0 deletions src/main/scala/controller/update/LaunchNewMissile.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package controller.update

import controller.Event
import controller.Event.LaunchMissileTo
import controller.update.Update.on
import model.World
import model.behavior.Moveable
import model.collisions.Collisionable
import monix.eval.Task

/**
* Object that return an update function for the world to be updated when the user launch a missile.
*/
object LaunchNewMissile:

/**
* Apply function used to update the world to be updated when the user launch a missile.
*
* @return an update function for the world to be updated when the user launch a missile.
*/
def apply(): Update = on[LaunchMissileTo] { (event: LaunchMissileTo, world: World) =>
Task {
val (ground, missile) = world.ground.shootMissile(event.position)
missile match
case Some(missile) => world.copy(collisionables = world.collisionables :+ missile, ground = ground)
case None => world.copy(collisionables = world.collisionables, ground = ground)
}
}
Loading

0 comments on commit ed7bdb2

Please sign in to comment.