Version initiale
This commit is contained in:
commit
808c9d7385
22 changed files with 403 additions and 0 deletions
5
.dockerignore
Normal file
5
.dockerignore
Normal file
|
@ -0,0 +1,5 @@
|
|||
target/
|
||||
.git/
|
||||
.idea/
|
||||
*.class
|
||||
*.log
|
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
logs
|
||||
target
|
||||
/.bsp
|
||||
/.idea
|
||||
/.idea_modules
|
||||
/.classpath
|
||||
/.project
|
||||
/.settings
|
||||
/RUNNING_PID
|
24
Dockerfile
Normal file
24
Dockerfile
Normal 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"]
|
BIN
app/assets/images/background.png
Normal file
BIN
app/assets/images/background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 116 KiB |
25
app/controllers/GameController.scala
Normal file
25
app/controllers/GameController.scala
Normal 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
7
app/models/Brick.scala
Normal file
|
@ -0,0 +1,7 @@
|
|||
package models
|
||||
|
||||
case class Brick(x: Int,
|
||||
y: Int,
|
||||
text: String,
|
||||
color: String,
|
||||
textColor: String)
|
6
app/views/game.scala.html
Normal file
6
app/views/game.scala.html
Normal 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
14
app/views/main.scala.html
Normal 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
1
build-local.sh
Normal file
|
@ -0,0 +1 @@
|
|||
docker build -t breakout-game .
|
16
build.sbt
Normal file
16
build.sbt
Normal 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
3
conf/application.conf
Normal file
|
@ -0,0 +1,3 @@
|
|||
play.filters.hosts {
|
||||
allowed = ["."]
|
||||
}
|
25
conf/bricks.json
Normal file
25
conf/bricks.json
Normal 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
50
conf/logback.xml
Normal 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
5
conf/routes
Normal 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
1
project/build.properties
Normal file
|
@ -0,0 +1 @@
|
|||
sbt.version=1.10.1
|
1
project/plugins.sbt
Normal file
1
project/plugins.sbt
Normal file
|
@ -0,0 +1 @@
|
|||
addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.5")
|
BIN
public/images/favicon.png
Normal file
BIN
public/images/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 687 B |
163
public/javascripts/game.js
Normal file
163
public/javascripts/game.js
Normal 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();
|
||||
});
|
2
public/javascripts/main.js
Normal file
2
public/javascripts/main.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
// Main application JavaScript file
|
||||
console.log('Main JavaScript file loaded');
|
0
public/stylesheets/main.css
Normal file
0
public/stylesheets/main.css
Normal file
1
run-docker.sh
Normal file
1
run-docker.sh
Normal file
|
@ -0,0 +1 @@
|
|||
docker run -p 9000:9000 breakout-game
|
45
test/controllers/HomeControllerSpec.scala
Normal file
45
test/controllers/HomeControllerSpec.scala
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue