Version initiale

This commit is contained in:
François Pelletier 2024-08-31 22:30:44 -04:00
commit 808c9d7385
22 changed files with 403 additions and 0 deletions

5
.dockerignore Normal file
View file

@ -0,0 +1,5 @@
target/
.git/
.idea/
*.class
*.log

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
logs
target
/.bsp
/.idea
/.idea_modules
/.classpath
/.project
/.settings
/RUNNING_PID

24
Dockerfile Normal file
View file

@ -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"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View file

@ -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))
}
}

7
app/models/Brick.scala Normal file
View file

@ -0,0 +1,7 @@
package models
case class Brick(x: Int,
y: Int,
text: String,
color: String,
textColor: String)

View file

@ -0,0 +1,6 @@
@()
@main("Arkanoid Game") {
<canvas id="gameCanvas" width="540" height="675"></canvas>
<script src="@routes.Assets.versioned("javascripts/game.js")"></script>
}

14
app/views/main.scala.html Normal file
View file

@ -0,0 +1,14 @@
@(title: String)(content: Html)
<!DOCTYPE html>
<html lang="en">
<head>
<title>@title</title>
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
</head>
<body>
@content
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
</body>
</html>

1
build-local.sh Normal file
View file

@ -0,0 +1 @@
docker build -t breakout-game .

16
build.sbt Normal file
View file

@ -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._"

3
conf/application.conf Normal file
View file

@ -0,0 +1,3 @@
play.filters.hosts {
allowed = ["."]
}

25
conf/bricks.json Normal file
View file

@ -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"}
]

50
conf/logback.xml Normal file
View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- https://www.playframework.com/documentation/latest/SettingsLogger -->
<!DOCTYPE configuration>
<configuration>
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
<import class="ch.qos.logback.classic.AsyncAppender"/>
<import class="ch.qos.logback.core.FileAppender"/>
<import class="ch.qos.logback.core.ConsoleAppender"/>
<appender name="FILE" class="FileAppender">
<file>${application.home:-.}/logs/application.log</file>
<encoder class="PatternLayoutEncoder">
<charset>UTF-8</charset>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{pekkoSource}) %msg%n</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ConsoleAppender">
<!--
On Windows, enabling Jansi is recommended to benefit from color code interpretation on DOS command prompts,
which otherwise risk being sent ANSI escape sequences that they cannot interpret.
See https://logback.qos.ch/manual/layouts.html#coloring
-->
<!-- <withJansi>true</withJansi> -->
<encoder class="PatternLayoutEncoder">
<charset>UTF-8</charset>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{pekkoSource}) %msg%n</pattern>
</encoder>
</appender>
<appender name="ASYNCFILE" class="AsyncAppender">
<appender-ref ref="FILE"/>
</appender>
<appender name="ASYNCSTDOUT" class="AsyncAppender">
<appender-ref ref="STDOUT"/>
</appender>
<logger name="play" level="INFO"/>
<logger name="application" level="DEBUG"/>
<root level="WARN">
<appender-ref ref="ASYNCFILE"/>
<appender-ref ref="ASYNCSTDOUT"/>
</root>
</configuration>

5
conf/routes Normal file
View file

@ -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)

1
project/build.properties Normal file
View file

@ -0,0 +1 @@
sbt.version=1.10.1

1
project/plugins.sbt Normal file
View file

@ -0,0 +1 @@
addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.5")

BIN
public/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

163
public/javascripts/game.js Normal file
View file

@ -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();
});

View file

@ -0,0 +1,2 @@
// Main application JavaScript file
console.log('Main JavaScript file loaded');

View file

1
run-docker.sh Normal file
View file

@ -0,0 +1 @@
docker run -p 9000:9000 breakout-game

View file

@ -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")
}
}
}