From 089586946094cc1b7b9ae977d89b7afd20991192 Mon Sep 17 00:00:00 2001 From: Andrea Cavalli Date: Thu, 23 May 2024 14:48:50 +0200 Subject: [PATCH] First commit --- .editorconfig | 11 ++ Dockerfile | 10 ++ README.adoc | 24 ++++ pom.xml | 122 ++++++++++++++++++ .../java/it/cavallium/connect4x4/App.java | 15 +++ .../it/cavallium/connect4x4/MainVerticle.java | 55 ++++++++ .../resources/webroot/static/connect4x4.js | 118 +++++++++++++++++ src/main/resources/webroot/static/index.html | 17 +++ src/main/resources/webroot/static/style.css | 23 ++++ .../connect4x4/TestMainVerticle.java | 22 ++++ 10 files changed, 417 insertions(+) create mode 100644 .editorconfig create mode 100644 Dockerfile create mode 100644 README.adoc create mode 100644 pom.xml create mode 100644 src/main/java/it/cavallium/connect4x4/App.java create mode 100644 src/main/java/it/cavallium/connect4x4/MainVerticle.java create mode 100644 src/main/resources/webroot/static/connect4x4.js create mode 100644 src/main/resources/webroot/static/index.html create mode 100644 src/main/resources/webroot/static/style.css create mode 100644 src/test/java/it/cavallium/connect4x4/TestMainVerticle.java diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3003493 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# EditorConfig is awesome: https://EditorConfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +end_of_line = lf +insert_final_newline = true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7e3dbbd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM maven:3-eclipse-temurin-22 AS build +WORKDIR /build +COPY --link src src +COPY --link pom.xml pom.xml +RUN mvn package + +FROM eclipse-temurin:22-jdk-alpine +WORKDIR /app +COPY --from=build --link /build/target/connect4x4-fat.jar app.jar +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..b05ec9b --- /dev/null +++ b/README.adoc @@ -0,0 +1,24 @@ += Connect4x4 + +image:https://img.shields.io/badge/vert.x-4.5.7-purple.svg[link="https://vertx.io"] + +== Building + +To launch your tests: +``` +mvn clean test +``` + +To package your application: +``` +mvn clean package +``` + +To run your application: +``` +mvn clean compile exec:java +``` + +== Help + +* https://vertx.io/docs/[Vert.x Documentation] diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..182986c --- /dev/null +++ b/pom.xml @@ -0,0 +1,122 @@ + + + 4.0.0 + + it.cavallium + connect4x4 + 1.0.0-SNAPSHOT + + + UTF-8 + + 3.8.1 + 3.2.4 + 2.22.2 + 3.0.0 + + 4.5.7 + 5.10.2 + + it.cavallium.connect4x4.MainVerticle + io.vertx.core.Launcher + + + + + + io.vertx + vertx-stack-depchain + ${vertx.version} + pom + import + + + + + + + io.vertx + vertx-web + + + io.vertx + vertx-rx-java3 + + + + io.vertx + vertx-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + + + + + maven-compiler-plugin + ${maven-compiler-plugin.version} + + 17 + + + + maven-shade-plugin + ${maven-shade-plugin.version} + + + package + + shade + + + + + + ${launcher.class} + ${main.verticle} + + + + + ${project.build.directory}/${project.artifactId}-fat.jar + + + + + + + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.codehaus.mojo + exec-maven-plugin + ${exec-maven-plugin.version} + + io.vertx.core.Launcher + + run + ${main.verticle} + + + + + + + + diff --git a/src/main/java/it/cavallium/connect4x4/App.java b/src/main/java/it/cavallium/connect4x4/App.java new file mode 100644 index 0000000..b0929f3 --- /dev/null +++ b/src/main/java/it/cavallium/connect4x4/App.java @@ -0,0 +1,15 @@ +package it.cavallium.connect4x4; + +import io.vertx.core.DeploymentOptions; +import io.vertx.core.ThreadingModel; +import io.vertx.core.Vertx; +import io.vertx.core.VertxOptions; + +public class App { + + public static void main(String[] args) throws InterruptedException { + var vertx = Vertx.builder().with(new VertxOptions().setPreferNativeTransport(true)).build(); + vertx.deployVerticle(new MainVerticle(), new DeploymentOptions().setThreadingModel(ThreadingModel.VIRTUAL_THREAD)); + Thread.currentThread().join(); + } +} diff --git a/src/main/java/it/cavallium/connect4x4/MainVerticle.java b/src/main/java/it/cavallium/connect4x4/MainVerticle.java new file mode 100644 index 0000000..ce83253 --- /dev/null +++ b/src/main/java/it/cavallium/connect4x4/MainVerticle.java @@ -0,0 +1,55 @@ +package it.cavallium.connect4x4; + +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Single; +import io.vertx.ext.bridge.PermittedOptions; +import io.vertx.ext.web.handler.FileSystemAccess; +import io.vertx.ext.web.handler.sockjs.SockJSBridgeOptions; +import io.vertx.rxjava3.core.AbstractVerticle; +import io.vertx.rxjava3.core.http.HttpServer; +import io.vertx.rxjava3.ext.web.Router; +import io.vertx.rxjava3.ext.web.handler.StaticHandler; +import io.vertx.rxjava3.ext.web.handler.sockjs.SockJSHandler; +import java.time.Duration; + +public class MainVerticle extends AbstractVerticle { + + private Single httpServer; + + @Override + public Completable rxStart() { + + vertx.eventBus().consumer("connect4x4.move") + .handler(event -> System.out.println("Received: " + event.body())); + + var server = vertx.createHttpServer(); + + SockJSHandler sockJSHandler = SockJSHandler.create(vertx); + SockJSBridgeOptions sockJSBridgeOptions = new SockJSBridgeOptions() + .addInboundPermitted(new PermittedOptions().setAddress("connect4x4.move")) + .addOutboundPermitted(new PermittedOptions().setAddress("connect4x4.moved")); + + Router router = Router.router(vertx); + router + .get("/*") + .handler(StaticHandler + .create(FileSystemAccess.RELATIVE,"webroot") + //.setDirectoryListing(true) + .setCachingEnabled(true) + .setAlwaysAsyncFS(true) + .setIndexPage("static/index.html") + .setSendVaryHeader(true) + .setEnableRangeSupport(true) + .setMaxAgeSeconds(Duration.ofSeconds(1).toSeconds())); + + router.get("/eventbus/*").subRouter(sockJSHandler.bridge(sockJSBridgeOptions)); + + this.httpServer = server.requestHandler(router).listen(8081).cache(); + return httpServer.ignoreElement(); + } + + @Override + public Completable rxStop() { + return httpServer.flatMapCompletable(HttpServer::close); + } +} diff --git a/src/main/resources/webroot/static/connect4x4.js b/src/main/resources/webroot/static/connect4x4.js new file mode 100644 index 0000000..b84bb44 --- /dev/null +++ b/src/main/resources/webroot/static/connect4x4.js @@ -0,0 +1,118 @@ +const chars = ["AN","AR","AT","CA","CH","CO","DE","DI","EL","EN","ER","ES","HE","IA","IL","IN","IO","LA","LE","LI","LL","MA","ME","NA","NE","NO","NT","OL","ON","OR","PE","RA","RE","RI","RO","SE","SI","SO","ST","TA","TE","TI","TO","TR","TT","UN","TO","RE","ER","ON","CO","DI","TA","EN","IN","TE","AT","RA","AN","NO","NT","ST","LA","AR","AL","OR","CH","RI","TI","IO","LE","DE","ES","NE","ME","TT","EL","PE","IL","UN","IA","LI","SE","SO","LL","SI","OL","RO","MA","CA","NA","TR","HE","ALE","ALL","ANC","AND","ANT","ARE","ATO","ATT","CHE","CHI","COM","CON","DEL","ELL","ENT","ERA","ERE","ESS","EST","ETT","GLI","ION","LLA","MEN","NON","NTE","NTI","NTO","OLO","ONE","ONO","PER","QUE","SON","STA","STO","TAT","TRA","TTO","UNA","VER","ZIO","ENT","CHE","ATO","PER","NTE","CON","ELL","STA","ARE","MEN","ION","DEL","LLA","TTO","TAT","ESS","ERE","ETT","EST","ONE","ONO","ZIO","NON","ERA","CHI","GLI","COM","TRA","STO","NTI","SON","VER","ATT","UNA","QUE","NTO","AND","ALL","OLO","ANC","ANT","ALE"]; +function randomID() { + var u8 = crypto.getRandomValues(new Uint8Array(3)); + return Array.from(u8).map(n => chars[n % chars.length]).reduce((a, b) => a + b); +} + +document.addEventListener("DOMContentLoaded", e => { + + const gameCode = document.getElementById("game-code"); + const gameCodeLink = document.getElementById("game-code-link"); + const newGameLink = document.getElementById("new-game-link"); + const mainBody = document.getElementById("main-body"); + + const eb = new EventBus(`${document.location.origin}/eventbus`); + eb.enableReconnect(true); + eb.onopen = function() {}; // Set up handlers here, will be called on initial connection and all reconnections + eb.onreconnect = function() {}; // Optional, will only be called on reconnections + + // Alternatively, pass in an options object + var options = { + vertxbus_reconnect_attempts_max: Infinity, // Max reconnect attempts + vertxbus_reconnect_delay_min: 1000, // Initial delay (in ms) before first reconnect attempt + vertxbus_reconnect_delay_max: 5000, // Max delay (in ms) between reconnect attempts + vertxbus_reconnect_exponent: 2, // Exponential backoff factor + vertxbus_randomization_factor: 0.5 // Randomization factor between 0 and 1 + }; + + + const connect4x4 = document.connect4x4 = { + clientId: randomID(), + gameId: null + } + + window.addEventListener("hashchange", e=> { + onHashChange(e.target.location.hash); + }) + + onHashChange(document.location.hash); + + function onHashChange(hash) { + if (hash != null && hash.length > 0) { + hash = hash.substring(1); + } else { + hash = ""; + } + if (hash.length === 0) { + if (connect4x4.gameId == null) { + onGameIdChange(randomID()); + } else { + console.warn("Game hash unchanged."); + } + } else if (hash.length > 32) { + console.error("Invalid page hash length", hash); + document.body.innerHTML = `

Invalid page hash length

Go back

`; + document.getElementById("hash-text").innerText = hash; + const redirectLink = document.location.origin; + document.getElementById("redirect-link").href = redirectLink; + setTimeout(() => { + document.location.href = redirectLink; + }, 5000); + } else if (connect4x4.gameId !== hash) { + if (connect4x4.gameId != null) { + console.log("Game id changed, reloading..."); + document.location.reload(); + } else { + onGameIdChange(hash); + } + } else { + console.warn("Game hash unchanged."); + } + } + + function onGameIdChange(gameId) { + connect4x4.gameId = gameId; + console.info("Game ID", gameId); + gameCode.innerText = gameId; + const gameLink = `${document.location.origin}#${gameId}`; + document.location.hash = gameId; + gameCodeLink.href = gameLink; + gameCodeLink.onclick = async e => { + if (navigator.share !== undefined) { + e.preventDefault(); + await navigator.share({ + title: "Connect 4x4", + text: "Play Connect 4x4 game with me", + url: gameLink + }).then(() => console.log("Shared!")).catch(err => { + console.error("Share failed", err); + navigateToGame(gameId); + }); + } else { + navigateToGame(gameId); + } + } + onGameLoad(); + } + + function navigateToGame(gameId) { + document.location.hash = gameId; + } + + function onGameLoad() { + mainBody.classList.add("js-loaded"); + } + + eb.onopen = () => { + +// set a handler to receive a message + eb.registerHandler('connect4x4.moved', (error, message) => { + console.log('received a message: ' + JSON.stringify(message)); + }); + +// send a message + eb.send('connect4x4.move', {name: 'tim', age: 587}); + + } +}) + diff --git a/src/main/resources/webroot/static/index.html b/src/main/resources/webroot/static/index.html new file mode 100644 index 0000000..cb4516b --- /dev/null +++ b/src/main/resources/webroot/static/index.html @@ -0,0 +1,17 @@ + + + + Connect 4x4 + + + + + + + +
+

Connect 4x4

+

Game code: [share] [new game]

+
+ + diff --git a/src/main/resources/webroot/static/style.css b/src/main/resources/webroot/static/style.css new file mode 100644 index 0000000..ee76bcb --- /dev/null +++ b/src/main/resources/webroot/static/style.css @@ -0,0 +1,23 @@ +body { + font-family: sans-serif; +} + +h1 { + user-select: none; +} + +#main-body:not(.js-loaded) { + visibility: hidden; +} + +#game-code { + white-space: pre; + font-family: monospace; + user-select: all; + background: #ffeb3b63; + color: black; + border-radius: 5px; + padding: 2px 5px; + font-size: 75%; + font-weight: bold; +} diff --git a/src/test/java/it/cavallium/connect4x4/TestMainVerticle.java b/src/test/java/it/cavallium/connect4x4/TestMainVerticle.java new file mode 100644 index 0000000..2f8617c --- /dev/null +++ b/src/test/java/it/cavallium/connect4x4/TestMainVerticle.java @@ -0,0 +1,22 @@ +package it.cavallium.connect4x4; + +import io.vertx.core.Vertx; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(VertxExtension.class) +public class TestMainVerticle { + + @BeforeEach + void deploy_verticle(Vertx vertx, VertxTestContext testContext) { + vertx.deployVerticle(new MainVerticle(), testContext.succeeding(id -> testContext.completeNow())); + } + + @Test + void verticle_deployed(Vertx vertx, VertxTestContext testContext) throws Throwable { + testContext.completeNow(); + } +}