commit 808c9d738511c9b68442ff422b9b18ab7d8fdb42 Author: François Pelletier Date: Sat Aug 31 22:30:44 2024 -0400 Version initiale diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ead37ac --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +target/ +.git/ +.idea/ +*.class +*.log \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dce7303 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +logs +target +/.bsp +/.idea +/.idea_modules +/.classpath +/.project +/.settings +/RUNNING_PID diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..36acd85 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# Use the official OpenJDK base image +FROM openjdk:11 + +# Install sbt +RUN echo "deb https://repo.scala-sbt.org/scalasbt/debian all main" | tee /etc/apt/sources.list.d/sbt.list +RUN echo "deb https://repo.scala-sbt.org/scalasbt/debian /" | tee /etc/apt/sources.list.d/sbt_old.list +RUN curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x2EE0EA64E40A89B84B2DF73499E82A75642AC823" | apt-key add +RUN apt-get update +RUN apt-get install sbt + +# Set the working directory in the container +WORKDIR /app + +# Copy the project files into the container +COPY . /app + +# Run sbt clean and compile +RUN sbt clean compile + +# Expose the port the app runs on +EXPOSE 9000 + +# Command to run the application +CMD ["sbt", "run"] \ No newline at end of file diff --git a/app/assets/images/background.png b/app/assets/images/background.png new file mode 100644 index 0000000..c2d515f Binary files /dev/null and b/app/assets/images/background.png differ diff --git a/app/controllers/GameController.scala b/app/controllers/GameController.scala new file mode 100644 index 0000000..1f6c5c6 --- /dev/null +++ b/app/controllers/GameController.scala @@ -0,0 +1,25 @@ +package controllers + +import javax.inject._ +import play.api.mvc._ +import play.api.libs.json._ +import models.Brick + +@Singleton +class GameController @Inject()(val controllerComponents: ControllerComponents) extends BaseController { + + implicit val brickWrites: Writes[Brick] = Json.writes[Brick] + implicit val brickReads: Reads[Brick] = Json.reads[Brick] + + def index(): Action[AnyContent] = Action { implicit request: Request[AnyContent] => + Ok(views.html.game()) + } + + def getBricks: Action[AnyContent] = Action { + + val source = scala.io.Source.fromFile("conf/bricks.json") + val jsonString = try source.mkString finally source.close() + val bricks = Json.parse(jsonString).as[List[Brick]] + Ok(Json.toJson(bricks)) + } +} diff --git a/app/models/Brick.scala b/app/models/Brick.scala new file mode 100644 index 0000000..0d16ccb --- /dev/null +++ b/app/models/Brick.scala @@ -0,0 +1,7 @@ +package models + +case class Brick(x: Int, + y: Int, + text: String, + color: String, + textColor: String) diff --git a/app/views/game.scala.html b/app/views/game.scala.html new file mode 100644 index 0000000..78e3fe0 --- /dev/null +++ b/app/views/game.scala.html @@ -0,0 +1,6 @@ +@() + +@main("Arkanoid Game") { + + +} \ No newline at end of file diff --git a/app/views/main.scala.html b/app/views/main.scala.html new file mode 100644 index 0000000..c6af152 --- /dev/null +++ b/app/views/main.scala.html @@ -0,0 +1,14 @@ +@(title: String)(content: Html) + + + + + @title + + + + + @content + + + \ No newline at end of file diff --git a/build-local.sh b/build-local.sh new file mode 100644 index 0000000..9d68b2a --- /dev/null +++ b/build-local.sh @@ -0,0 +1 @@ +docker build -t breakout-game . \ No newline at end of file diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..8b88627 --- /dev/null +++ b/build.sbt @@ -0,0 +1,16 @@ +name := """breakout-custom""" + +version := "1.0-SNAPSHOT" + +lazy val root = (project in file(".")).enablePlugins(PlayScala) + +scalaVersion := "2.13.14" + +libraryDependencies += guice +libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "7.0.0" % Test + +// Adds additional packages into Twirl +//TwirlKeys.templateImports += "com.example.controllers._" + +// Adds additional packages into conf/routes +// play.sbt.routes.RoutesKeys.routesImport += "com.example.binders._" diff --git a/conf/application.conf b/conf/application.conf new file mode 100644 index 0000000..3c1ffc6 --- /dev/null +++ b/conf/application.conf @@ -0,0 +1,3 @@ +play.filters.hosts { + allowed = ["."] +} diff --git a/conf/bricks.json b/conf/bricks.json new file mode 100644 index 0000000..2dd023b --- /dev/null +++ b/conf/bricks.json @@ -0,0 +1,25 @@ +[ + {"x": 29, "y": 50, "text": "LinkedIn", "color": "#0077B5", "textColor": "#FFFFFF"}, + {"x": 127, "y": 50, "text": "GitHub", "color": "#24292E", "textColor": "#FFFFFF"}, + {"x": 225, "y": 50, "text": "Slack", "color": "#4A154B", "textColor": "#FFFFFF"}, + {"x": 323, "y": 50, "text": "Trello", "color": "#0079BF", "textColor": "#FFFFFF"}, + {"x": 421, "y": 50, "text": "Asana", "color": "#F06A6A", "textColor": "#FFFFFF"}, + + {"x": 29, "y": 88, "text": "Upwork", "color": "#6FDA44", "textColor": "#FFFFFF"}, + {"x": 127, "y": 88, "text": "Fiverr", "color": "#1DBF73", "textColor": "#FFFFFF"}, + {"x": 225, "y": 88, "text": "Zoom", "color": "#2D8CFF", "textColor": "#FFFFFF"}, + {"x": 323, "y": 88, "text": "Dropbox", "color": "#0061FF", "textColor": "#FFFFFF"}, + {"x": 421, "y": 88, "text": "Notion", "color": "#000000", "textColor": "#FFFFFF"}, + + {"x": 29, "y": 126, "text": "X", "color": "#000000", "textColor": "#FFFFFF"}, + {"x": 127, "y": 126, "text": "Stripe", "color": "#008CDD", "textColor": "#FFFFFF"}, + {"x": 225, "y": 126, "text": "Mailchimp", "color": "#FFE01B", "textColor": "#000000"}, + {"x": 323, "y": 126, "text": "Canva", "color": "#00C4CC", "textColor": "#FFFFFF"}, + {"x": 421, "y": 126, "text": "Google", "color": "#4285F4", "textColor": "#FFFFFF"}, + + {"x": 29, "y": 164, "text": "Facebook", "color": "#1877F2", "textColor": "#FFFFFF"}, + {"x": 127, "y": 164, "text": "Instagram", "color": "#E4405F", "textColor": "#FFFFFF"}, + {"x": 225, "y": 164, "text": "YouTube", "color": "#FF0000", "textColor": "#FFFFFF"}, + {"x": 323, "y": 164, "text": "TikTok", "color": "#000000", "textColor": "#FFFFFF"}, + {"x": 421, "y": 164, "text": "Pinterest", "color": "#BD081C", "textColor": "#FFFFFF"} +] \ No newline at end of file diff --git a/conf/logback.xml b/conf/logback.xml new file mode 100644 index 0000000..ab6c2b1 --- /dev/null +++ b/conf/logback.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + ${application.home:-.}/logs/application.log + + UTF-8 + %d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{pekkoSource}) %msg%n + + + + + + + + UTF-8 + %d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{pekkoSource}) %msg%n + + + + + + + + + + + + + + + + + + + + diff --git a/conf/routes b/conf/routes new file mode 100644 index 0000000..ec37fe3 --- /dev/null +++ b/conf/routes @@ -0,0 +1,5 @@ +GET /game controllers.GameController.index() +GET /bricks controllers.GameController.getBricks() + +# Map static resources from the /public folder to the /assets URL path +GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) \ No newline at end of file diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..ee4c672 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.10.1 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..19161b9 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.5") diff --git a/public/images/favicon.png b/public/images/favicon.png new file mode 100644 index 0000000..c7d92d2 Binary files /dev/null and b/public/images/favicon.png differ diff --git a/public/javascripts/game.js b/public/javascripts/game.js new file mode 100644 index 0000000..2a107e6 --- /dev/null +++ b/public/javascripts/game.js @@ -0,0 +1,163 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); + +let bricks = []; +let paddle = { x: 175, y: 510, width: 75, height: 15 }; +let ball = { x: 200, y: 370, dx: 4, dy: -4, radius: 8 }; +const floorY = 525; // Define the floor position + +let isPaused = false; + +function togglePause() { + isPaused = !isPaused; + if (!isPaused) { + requestAnimationFrame(update); + } +} + +function drawBricks() { + bricks.forEach(brick => { + ctx.fillStyle = brick.color; + ctx.fillRect(brick.x, brick.y, 90, 30); + ctx.fillStyle = brick.textColor; + ctx.font = '14px Arial'; + ctx.fillText(brick.text, brick.x + 10, brick.y + 20); + }); +} + +function drawPaddle() { + ctx.fillStyle = '#003333'; + ctx.fillRect(paddle.x, paddle.y, paddle.width, paddle.height); +} + +function drawBall() { + ctx.beginPath(); + ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2); + ctx.fillStyle = '#003333'; + ctx.fill(); + ctx.closePath(); +} + +function movePaddle(e) { + if (!isPaused) { + let relativeX = e.clientX - canvas.offsetLeft; + if (relativeX > 0 && relativeX < canvas.width) { + paddle.x = relativeX - paddle.width / 2; + } + } +} + +function collisionDetection() { + bricks.forEach((brick, index) => { + if ( + ball.x > brick.x && + ball.x < brick.x + 90 && // Changed from 50 to 90 + ball.y > brick.y && + ball.y < brick.y + 30 // Changed from 20 to 30 + ) { + ball.dy = -ball.dy; + bricks.splice(index, 1); + } + }); +} + +function drawPauseScreen() { + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = 'white'; + ctx.font = '30px Arial'; + ctx.fillText('PAUSED', canvas.width / 2 - 50, canvas.height / 2); +} + +function update() { + + if (isPaused) { + drawPauseScreen(); + return; + } + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw background image + let background = new Image(); + background.src = '/assets/images/background.png'; + ctx.drawImage(background, 0, 0, canvas.width, canvas.height); + + // Draw floor line + ctx.beginPath(); + ctx.moveTo(0, floorY); + ctx.lineTo(canvas.width, floorY); + ctx.strokeStyle = 'white'; + ctx.stroke(); + + drawBricks(); + drawPaddle(); + drawBall(); + + collisionDetection(); + + if (bricks.length === 0) { + alert('Congratulations! You won!'); + document.location.reload(); + return; + } + + // Bounce off side walls + if (ball.x + ball.dx > canvas.width - ball.radius || ball.x + ball.dx < ball.radius) { + ball.dx = -ball.dx; + } + + // Bounce off top wall + if (ball.y + ball.dy < ball.radius) { + ball.dy = -ball.dy; + } else if (ball.y + ball.dy > floorY - ball.radius) { // Changed to use floorY + if (ball.x > paddle.x && ball.x < paddle.x + paddle.width) { + // Calculate where the ball hit the paddle + let hitPos = (ball.x - paddle.x) / paddle.width; + + // Change angle based on where the ball hit the paddle + let angle = hitPos * Math.PI - Math.PI/2; + + // Add some randomness to the angle + angle += (Math.random() - 0.5) * Math.PI/4; + + // Set new velocity based on this angle + let speed = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy); + ball.dx = Math.cos(angle) * speed; + ball.dy = -Math.abs(Math.sin(angle) * speed); // Ensure the ball always goes up + } else { + alert('GAME OVER'); + document.location.reload(); + } + } + + ball.x += ball.dx; + ball.y += ball.dy; + + requestAnimationFrame(update); +} + +document.addEventListener('mousemove', movePaddle, false); + +document.addEventListener('keydown', function(e) { + if (e.key === 'p' || e.key === 'P') { + togglePause(); + } +}); + +// Create pause button +const pauseButton = document.createElement('button'); +pauseButton.textContent = 'Pause'; +pauseButton.style.position = 'absolute'; +pauseButton.style.top = '10px'; +pauseButton.style.left = '10px'; +document.body.appendChild(pauseButton); + +pauseButton.addEventListener('click', togglePause); + +fetch('/bricks') + .then(response => response.json()) + .then(data => { + bricks = data; + update(); + }); \ No newline at end of file diff --git a/public/javascripts/main.js b/public/javascripts/main.js new file mode 100644 index 0000000..e29190e --- /dev/null +++ b/public/javascripts/main.js @@ -0,0 +1,2 @@ +// Main application JavaScript file +console.log('Main JavaScript file loaded'); diff --git a/public/stylesheets/main.css b/public/stylesheets/main.css new file mode 100644 index 0000000..e69de29 diff --git a/run-docker.sh b/run-docker.sh new file mode 100644 index 0000000..f241bf2 --- /dev/null +++ b/run-docker.sh @@ -0,0 +1 @@ +docker run -p 9000:9000 breakout-game \ No newline at end of file diff --git a/test/controllers/HomeControllerSpec.scala b/test/controllers/HomeControllerSpec.scala new file mode 100644 index 0000000..9794755 --- /dev/null +++ b/test/controllers/HomeControllerSpec.scala @@ -0,0 +1,45 @@ +package controllers + +import org.scalatestplus.play._ +import org.scalatestplus.play.guice._ +import play.api.test._ +import play.api.test.Helpers._ + +/** + * Add your spec here. + * You can mock out a whole application including requests, plugins etc. + * + * For more information, see https://www.playframework.com/documentation/latest/ScalaTestingWithScalaTest + */ +class HomeControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting { + + "HomeController GET" should { + + "render the index page from a new instance of controller" in { + val controller = new HomeController(stubControllerComponents()) + val home = controller.index().apply(FakeRequest(GET, "/")) + + status(home) mustBe OK + contentType(home) mustBe Some("text/html") + contentAsString(home) must include ("Welcome to Play") + } + + "render the index page from the application" in { + val controller = inject[HomeController] + val home = controller.index().apply(FakeRequest(GET, "/")) + + status(home) mustBe OK + contentType(home) mustBe Some("text/html") + contentAsString(home) must include ("Welcome to Play") + } + + "render the index page from the router" in { + val request = FakeRequest(GET, "/") + val home = route(app, request).get + + status(home) mustBe OK + contentType(home) mustBe Some("text/html") + contentAsString(home) must include ("Welcome to Play") + } + } +}