diff --git a/android/tinySSB/app/build.gradle b/android/tinySSB/app/build.gradle
index 420aa20..021680d 100644
--- a/android/tinySSB/app/build.gradle
+++ b/android/tinySSB/app/build.gradle
@@ -12,7 +12,7 @@ android {
defaultConfig {
applicationId "nz.scuttlebutt.tremolavossbol"
- minSdk 23
+ minSdk 24
targetSdkVersion 30
versionCode 1
versionName "0.1"
diff --git a/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/BS-Icon.jpeg b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/BS-Icon.jpeg
new file mode 100644
index 0000000..9748a15
Binary files /dev/null and b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/BS-Icon.jpeg differ
diff --git a/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battelship/battleship.svg b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/battleship.svg
similarity index 100%
rename from android/tinySSB/app/src/main/assets/web/games/dpi24-06-battelship/battleship.svg
rename to android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/battleship.svg
diff --git a/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/duel.css b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/duel.css
new file mode 100644
index 0000000..2407354
--- /dev/null
+++ b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/duel.css
@@ -0,0 +1,341 @@
+/*
+ games/dpi24-06-battleship/duel.css
+*/
+
+/* -------------DUEL-------------------*/
+#duelInviteContainer {
+ display: none;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background-color: #ffffff;
+ padding: 20px;
+ border: 2px solid #000000;
+ z-index: 1000;
+ }
+
+/*
+.scroll-container {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ overflow-y: auto;
+ max-height: 350px;
+}
+*/
+
+.square {
+ width: 150px;
+ height: 150px;
+ margin: 10px 0;
+ padding: 0;
+ border: none;
+ background: none;
+ cursor: pointer;
+}
+
+.square img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+button.square:focus {
+ outline: none; /* removes focus */
+}
+
+button.square:active {
+ transform: scale(0.95); /* makes button smaller when clicked */
+}
+
+.duel-button-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 10px;
+}
+
+.duel-button {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ width: 100%;
+ max-width: 550px;
+ padding: 10px;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ background-color: #f9f9f9;
+ text-align: left;
+ cursor: pointer;
+ transition: background-color 0.3s;
+}
+
+.no-duel-box {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ max-width: 600px;
+ padding: 10px;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ background-color: #f9f9f9;
+ text-align: center;
+ cursor: default; /* cursor set to inactive */
+}
+
+.duel-button-invited {
+ background-color: #d1ecf1; /* light blue */
+}
+
+.duel-button-running {
+ background-color: #fff3cd; /* light green */
+}
+
+.duel-button-waiting {
+ background-color: #fff3cd; /* light yellow */
+}
+
+.duel-button-won {
+ background-color: #c3e6cb; /* green */
+}
+
+.duel-button-lost {
+ background-color: #f8d7da; /* light red */
+}
+
+.duel-button-stopped {
+ background-color: #e2e3e5; /* light grey */
+}
+
+.duel-button:hover {
+ background-color: #e0e0e0;
+}
+
+.duel-image {
+ width: 50px;
+ height: 50px;
+ margin-right: 10px;
+}
+
+.duel-text {
+ font-size: 18px;
+ font-weight: bold;
+}
+
+.board {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.battleshipsTopRow {
+ background: #50A4D3;
+ color: white;
+ font-weight: bold;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.battleshipsLeftRow {
+ background: #50A4D3;
+ color: white;
+ font-weight: bold;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.displays {
+ width: 40vh;
+}
+
+.grid {
+ aspect-ratio: 1 / 1;
+ display: grid;
+ grid-template-columns: repeat(11, 1fr);
+ grid-template-rows: repeat(11, 1fr);
+
+}
+
+.field {
+ background: #ACCADF;
+ border-right: 1px solid Blue;
+ border-top: 1px solid Blue;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.hole {
+ width: 30%;
+ height: 30%;
+ border: 1px solid Blue;
+ border-radius: 50%;
+}
+
+.ship {
+ background: Black;
+ border-right: 1px solid Blue;
+ border-top: 1px solid Blue;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.miss {
+ background: Blue;
+ border-right: 1px solid Blue;
+ border-top: 1px solid Blue;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.hit {
+ background: Orange;
+ border-right: 1px solid Blue;
+ border-top: 1px solid Blue;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.sunken {
+ background: Red;
+ border-right: 1px solid Blue;
+ border-top: 1px solid Blue;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.field_clicked {
+ -webkit-animation: grow 0.5s;
+}
+
+@-webkit-keyframes grow {
+ from {
+ width: 0%;
+ height: 0%;
+ }
+ to {
+ width: 30%;
+ height: 30%;
+ }
+}
+
+.battleships_control {
+ display: flex;
+ justify-content: center;
+}
+
+.battleships_info {
+ margin-top: 10px;
+ width: 70%;
+ padding: 15px 25px;
+ font-size: 1.5em;
+ text-align: center;
+ outline: none;
+ color: #fff;
+ background-color: #4CAF50;
+ border: none;
+ border-radius: 15px;
+}
+
+.battleships_placer {
+ display: flex;
+ justify-content: center;
+}
+
+.battleships_length {
+ width: 35vw;
+ text-align: center;
+ font-size: 1.3em;
+ padding: 15px;
+ margin-top: 10px;
+ background-color: #00c9ff;
+ border: none;
+ border-radius: 15px;
+ color: #fff;
+}
+
+.battleships_turn {
+ width: 70vw;
+ text-align: center;
+ font-size: 1.3em;
+ padding: 15px;
+ margin-top: 10px;
+ border: 1px solid #000;
+ border-radius: 15px;
+ color: #fff;
+}
+
+.turn-won {
+ background-color: green;
+ color: white;
+}
+
+.turn-lost {
+ background-color: red;
+ color: white;
+}
+
+.turn-your {
+ background-color: d4edda;
+ color: white;
+}
+
+.turn-enemy {
+ background-color: f8d7da;
+ color: white;
+}
+
+.turn-default {
+ background-color: #50A4D3;
+ color: #fff;
+}
+
+.battleships_orientation_button {
+ margin-top: 10px;
+ width: 35vw;
+ padding: 15px;
+ font-size: 1.3em;
+ text-align: center;
+ outline: none;
+ color: #fff;
+ background-color: #4CAF50;
+ border: none;
+ border-radius: 15px;
+ box-shadow: 0 9px #999;
+}
+
+.battleships_orientation_button:active {
+ background-color: #3e8e41;
+ box-shadow: 0 5px #666;
+ transform: translateY(4px);
+}
+
+.battleships_big_button {
+ margin-top: 10px;
+ width: 70%;
+ padding: 15px 25px;
+ font-size: 1.5em;
+ text-align: center;
+ outline: none;
+ color: #fff;
+ background-color: #4CAF50;
+ border: none;
+ border-radius: 15px;
+ box-shadow: 0 9px #999;
+}
+
+.battleships_big_button:active {
+ background-color: #3e8e41;
+ box-shadow: 0 5px #666;
+ transform: translateY(4px);
+}
+
+/* eof */
\ No newline at end of file
diff --git a/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/duel.js b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/duel.js
new file mode 100644
index 0000000..1afa24a
--- /dev/null
+++ b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/duel.js
@@ -0,0 +1,562 @@
+"use strict";
+
+var battleships_turn = false
+var battleships_horizontal = true
+var battleships_ship_positions = ""
+var battleship_ship_lengths = [2, 3, 3, 4 ,5]
+var battleship_status = "STOPPED"
+var battleship_timestamp = "0"
+
+var owner = "-"
+var peer = "-"
+var game = "-"
+
+/*
+* This method opens the selection menu for possible games needed in order to send an invite.
+*/
+
+function duel_openDuels() {
+ return;
+ console.log('duel_openDuels()')
+ setScenario('duels')
+ /*
+ // closeOverlay();
+ var duelInviteContainer = document.getElementById('div:duelInviteContainer');
+ if (duelInviteContainer) {
+ duelInviteContainer.style.display = 'block';
+ } else {
+ console.error('Duel-Invite-Container not found!');
+ }
+ */
+}
+
+/**
+* This method gets called if you click on an icon in the invitemenu.
+*/
+function inviteForDuel(gameType) {
+ closeDuelOverlay();
+ var stringToEncode = "BSH INV " + myId;
+ console.log('Invited battleship ' + JSON.stringify(stringToEncode))
+ backend("games "+ stringToEncode);
+}
+
+/**
+* This function closes the invitemenu. It gets called by the Cancel-Button.
+*/
+function closeDuelOverlay() {
+ var duelInviteContainer = document.getElementById('div:duelInviteContainer');
+ if (duelInviteContainer) {
+ duelInviteContainer.style.display = 'none';
+ }
+}
+
+// ---------- GAME-SCREEN -----------
+
+function update_game_gui(response) {
+ console.log("BSH updating GUI ... " + JSON.stringify(response))
+ if (curr_scenario == 'battleships') {
+ if (window.GamesHandler && typeof window.GamesHandler.getInstanceDescriptorFromFids === 'function' && typeof window.GamesHandler.getInstanceDescriptorFromFid === 'function') {
+ var instanceDescriptor = ""
+ var responseList = response.split(" ")
+ console.log("BSH updating GUI after split")
+ if (responseList.length > 1) {
+ console.log("BSH updating GUI > 1")
+ if (owner == responseList[0] && peer == "-" && battleship_status == "INVITED") {
+ console.log("BSH updating GUI setting peer")
+ peer = responseList[1]
+ }
+ }
+ console.log("BSH updating GUI before instanceDescriptor")
+ if (peer == "-") {
+ instanceDescriptor = window.GamesHandler.getInstanceDescriptorFromFid(game, owner)
+ } else {
+ instanceDescriptor = window.GamesHandler.getInstanceDescriptorFromFids(game, owner, peer, battleship_timestamp)
+ }
+
+ console.log("BSH update_gui ", JSON.stringify(instanceDescriptor))
+ var instanceList = instanceDescriptor.split(" ")
+ battleship_status = instanceList[4]
+ battleship_timestamp = instanceList[3]
+ if (instanceList.length < 6) { return }
+ var playerTurn = instanceList[5]
+ if (playerTurn == "1") {
+ battleships(true, instanceList[6])
+ } else {
+ battleships(false, instanceList[6])
+ }
+ }
+ } else if (curr_scenario == 'duels') {
+ show_duels()
+ }
+}
+
+
+// The main function that launches the game GUI
+function battleships(turn, ships_fired_recv) {
+ var args = ships_fired_recv.split("^");
+ battleships_turn = turn;
+ console.log("Called BSH GUI:", JSON.stringify(ships_fired_recv));
+
+ setScenario("battleships")
+
+ /*
+ var c = document.getElementById("conversationTitle");
+ c.style.display = null;
+ c.innerHTML = "
My Battleships
";
+ */
+ battleships_load_config(battleship_status, args[0], args[1], args[2]);
+}
+
+/**
+* This function is called if a user wants to quit the game.
+*/
+function quit_bsh() {
+ backend("games BSH DUELQUIT " + owner + " " + peer);
+ reset_battleship_mode();
+ show_duels()
+}
+
+
+// Gets called when the user clicks on a square of the bottom field
+function battleship_bottom_field_click(i) {
+ console.log("BSH registered Shot: ", JSON.stringify(i))
+ var square = document.getElementById("battleships:bottom" + i)
+ // Do nothing if it is not our turn
+ if (!battleships_turn) {
+ return
+ } else {
+ backend("games BSH SHOT " + owner + " " + peer + " " + (((i % 10) + 9) % 10) + "" + Math.floor((i - 1) / 10))
+ battleships_set_turn(false)
+ battleships_show_turn()
+ }
+ square.childNodes[0].className = "hole field_clicked"
+ setTimeout(function () {
+ square.childNodes[0].className = "hole"
+ }, 100);
+}
+
+function battleship_top_field_click(i) {
+ // ignore
+}
+
+// Generates the HTML of the playing field
+function battleships_setup() {
+ console.log("BSH_setup()", JSON.stringify(battleship_status));
+ battleships_hide_controls()
+ var board = document.getElementById("battleships:board")
+ var topgrid = document.getElementById("battleships:topgrid")
+ topgrid.innerHTML = ""
+ topgrid.insertAdjacentHTML("beforeend", " ")
+ for (var i = 1; i < 11; i++) {
+ topgrid.insertAdjacentHTML("beforeend", "" + i + " ")
+ }
+ var counter = 1;
+ for (var i = 12; i <= 121; i++) {
+ if ((i % 11) - 1 == 0) {
+ topgrid.insertAdjacentHTML("beforeend", "" + String.fromCharCode(64 + (i - 1) / 11).toUpperCase() + " ")
+ } else {
+ topgrid.insertAdjacentHTML("beforeend", " ")
+ counter++
+ }
+ }
+ var bottomgrid = document.getElementById("battleships:bottomgrid")
+ bottomgrid.innerHTML = ""
+ bottomgrid.insertAdjacentHTML("beforeend", " ")
+ for (var i = 1; i < 11; i++) {
+ bottomgrid.insertAdjacentHTML("beforeend", "" + i + " ")
+ }
+ var counter = 1;
+ for (var i = 12; i <= 121; i++) {
+ if ((i % 11) - 1 == 0) {
+ bottomgrid.insertAdjacentHTML("beforeend", "" + String.fromCharCode(64 + (i - 1) / 11).toUpperCase() + " ")
+ } else {
+ bottomgrid.insertAdjacentHTML("beforeend", " ")
+ counter++
+ }
+ }
+}
+
+function battleships_set_turn(is_turn) {
+ battleships_turn = is_turn
+}
+
+// Hides everything below the two grids
+function battleships_hide_controls() {
+ document.getElementById("battleships:turn").style.display = "none"
+}
+
+// Shows the turn indicator below the grid
+function battleships_show_turn() {
+ console.log("BSH Showing Turn...");
+ battleships_hide_controls();
+ var peerId = myId;
+ var turn = document.getElementById("battleships:turn");
+ turn.style.display = null;
+
+ console.log("BSH Determining what to display as turn ...");
+
+ // Set default styles
+ turn.style.width = '70vw';
+ turn.style.textAlign = 'center';
+ turn.style.fontSize = '1.3em';
+ turn.style.padding = '15px';
+ turn.style.marginTop = '10px';
+ turn.style.border = '1px solid #000';
+ turn.style.borderRadius = '15px';
+ turn.style.backgroundColor = '#50A4D3';
+ turn.style.color = '#fff';
+
+ if (peerId == owner || peerId == peer) {
+ if (battleship_status == "WON") {
+ turn.innerHTML = "You Won!";
+ turn.style.backgroundColor = 'green';
+ turn.style.color = 'white';
+ } else if (battleship_status == "LOST") {
+ turn.innerHTML = "You Lost!";
+ turn.style.backgroundColor = 'red';
+ turn.style.color = 'white';
+ } else if (battleship_status == "STOPPED") {
+ turn.innerHTML = "The game is stopped!";
+ } else if (battleship_status == "INVITED") {
+ console.log("BSH invite-button init ...");
+ turn.innerHTML = "Waiting for other!";
+ } else if (battleship_status == "WAITING") {
+ if (peerId == "-") {
+ turn.innerHTML = "Waiting ...";
+ }
+ } else if (battleship_status == "RUNNING") {
+ if (battleships_turn) {
+ turn.innerHTML = "Your Turn";
+ turn.style.backgroundColor = '#d4edda'; // Light green
+ turn.style.color = 'black';
+ } else {
+ turn.innerHTML = "Enemy Turn";
+ turn.style.backgroundColor = '#f8d7da'; // Light red
+ turn.style.color = 'black';
+ }
+ }
+ } else {
+ if (battleship_status == "WON") {
+ if (battleships_turn) {
+ turn.innerHTML = "Owner has Won!";
+ } else {
+ turn.innerHTML = "Peer has Won!";
+ }
+ turn.style.backgroundColor = 'green';
+ turn.style.color = 'white';
+ } else if (battleship_status == "LOST") {
+ if (battleships_turn) {
+ turn.innerHTML = "Owner has Lost!";
+ } else {
+ turn.innerHTML = "Peer has Lost!";
+ }
+ turn.style.backgroundColor = 'red';
+ turn.style.color = 'white';
+ } else {
+ if (battleships_turn) {
+ turn.innerHTML = "Owner's Turn!";
+ } else {
+ turn.innerHTML = "Peer's Turn!";
+ }
+ }
+ }
+}
+
+
+// Displays the config given from the backend. The format of the config is
+// well described in the backend.
+function battleships_load_config(state, ships, deliv, recv) {
+ document.getElementById("conversationTitle").innerHTML = "My Battleships
";
+ battleships_ship_positions = ""
+ console.log("BSH_load_config", JSON.stringify(state + " " + ships + " " + recv + " " + deliv));
+ if (state === "STOPPED") {
+ document.getElementById("conversationTitle").innerHTML = "Stopped Game!
";
+ battleships_setup()
+ battleships_show_turn()
+ } else if (state === "INVITED") {
+ battleships_setup()
+ battleships_show_turn()
+ //return
+ } else if (state === "WON") {
+ document.getElementById("conversationTitle").innerHTML = "You Won!
";
+ battleships_setup()
+ battleships_show_turn()
+ } else if (state === "LOST") {
+ document.getElementById("conversationTitle").innerHTML = "You Lost!
";
+ battleships_setup()
+ battleships_show_turn()
+ } else if (state === "RUNNING") {
+ battleships_setup()
+ battleships_show_turn()
+ }
+
+ console.log("BSH chunking ships now ...", JSON.stringify(ships))
+ var shipPositions = splitIntoChunks(ships, 3)
+ console.log("BSH chunky ships: ", JSON.stringify(shipPositions))
+ for (var i = 0; i < shipPositions.length; i++) {
+ console.log("BSH Processing Ship: ", shipPositions[i])
+ var ship = shipPositions[i]
+ var x = parseInt(ship.charAt(0))
+ var y = parseInt(ship.charAt(1))
+ if (x != -1) {
+ battleships_ship_positions = battleships_ship_positions + "" + ship
+ }
+ var direction = ship.slice(2, 100)
+ for (var j = 0; j < battleship_ship_lengths[i]; j++) {
+ var position = y * 10 + x + 1
+ if (direction === "U") {
+ position = position - 10 * j
+ } else if (direction === "D") {
+ position = position + 10 * j
+ } else if (direction === "L") {
+ position = position - j
+ } else if (direction === "R") {
+ position = position + j
+ }
+ var field = document.getElementById("battleships:top" + position)
+ field.className = "ship"
+ field.innerHTML = ""
+ field.onclick = null
+ }
+ }
+ console.log("BSH parsing shotsFired ", JSON.stringify(deliv))
+ var shots_fired = splitIntoChunks(deliv, 3)
+ for (var i = 0; i < shots_fired.length; i++) {
+ var shot = shots_fired[i]
+ if (shot === "") {
+ continue
+ }
+ var x = parseInt(shot.charAt(0))
+ var y = parseInt(shot.charAt(1))
+ var outcome = shot.slice(2, 100)
+ var position = y * 10 + x + 1
+ var field = document.getElementById("battleships:bottom" + position)
+ if (outcome === "M") {
+ field.className = "miss"
+ } else if (outcome === "H") {
+ field.className = "hit"
+ } else if (outcome === "S") {
+ field.className = "sunken"
+ }
+ field.innerHTML = ""
+ field.onclick = null
+ }
+ console.log("BSH Parsing Shots Received ", JSON.stringify(recv))
+ var shots_received = splitIntoChunks(recv, 3)
+ for (var i = 0; i < shots_received.length; i++) {
+ var shot = shots_received[i]
+ if (shot === "") {
+ continue
+ }
+ var x = parseInt(shot.charAt(0))
+ var y = parseInt(shot.charAt(1))
+ var outcome = shot.slice(2, 100)
+ var position = y * 10 + x + 1
+ var field = document.getElementById("battleships:top" + position)
+ if (outcome === "M") {
+ field.className = "miss"
+ } else if (outcome === "H") {
+ field.className = "hit"
+ } else if (outcome === "S") {
+ field.className = "sunken"
+ }
+ field.innerHTML = ""
+ field.onclick = null
+ }
+ if (state === "WAITING") {
+ battleships_show_turn()
+ return
+ }
+ battleships_show_turn()
+}
+
+/*
+* This method parses the ships into readable format.
+*/
+function splitIntoChunks(str, chunkSize) {
+ const chunks = [];
+ for (let i = 0; i < str.length; i += chunkSize) {
+ chunks.push(str.substring(i, i + chunkSize));
+ }
+ return chunks;
+}
+
+function reset_battleship_mode() {
+ battleships_turn = null
+ battleships_ship_positions = ""
+ battleship_status = "STOPPED"
+ battleship_timestamp = "0"
+ game = "-"
+
+ owner = "-"
+ peer = "-"
+}
+
+
+function show_duels() {
+ setScenario('duels');
+ let c = document.getElementById("conversationTitle");
+ c.style.display = null;
+ c.innerHTML = "Battleship Duels
";
+ var container = document.getElementById("duels-container");
+ container.innerHTML = "";
+
+ console.log('show_duels ' + JSON.stringify("gamelist before"));
+ var gameListString = "";
+// var gameListString = "BSH ownerid1 participantid1 12 STOPPED"
+ if (window.GamesHandler && typeof window.GamesHandler.createInstanceList === 'function') {
+ gameListString = window.GamesHandler.createInstanceList();
+ console.log('show_duels - gamelist received: ' + gameListString);
+ } else {
+ console.error("GamesHandler.createInstanceList is not a function");
+ }
+
+ if (gameListString === "") {
+ console.log('show_duels ' + JSON.stringify("No active duels found."));
+ var noDuelDiv = document.createElement("div");
+ noDuelDiv.className = "no-duel-box";
+ noDuelDiv.innerHTML = "No active duels available...";
+ container.appendChild(noDuelDiv);
+ } else {
+ var gameList = gameListString.split('$');
+ gameList.forEach(function(game) {
+ var gameParts = game.split(" ");
+ var gameName = gameParts[0];
+ var ownerName = gameParts[1];
+ //var ownerAlias = tremola.contacts[owner].alias;
+ var participantName = gameParts[2];
+ //var participantAlias = tremola.contacts[participant].alias;
+ var startTimeRaw = parseInt(gameParts[3]);;
+ // Format start time
+ var date = new Date(startTimeRaw);
+ var options = {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+ second: 'numeric',
+ hour12: true
+ };
+ var startTime = new Intl.DateTimeFormat('en-US', options).format(date);
+ var state = gameParts[4];
+ console.log('My Id: ' + JSON.stringify(myId));
+ var suffix = ".ed25519";
+ if (ownerName == myId) {
+ ownerName = "Me"
+ participantName = id2b32(participantName);
+ } else if (participantName == myId) {
+ participantName = "Me";
+ ownerName = id2b32(ownerName);
+ } else {
+ participantName = id2b32(participantName);
+ ownerName = id2b32(ownerName);
+ }
+ var turn = gameList[5];
+ var ships_rec_delivered = gameList[6];
+
+ console.log('Game-Container for: ' + JSON.stringify(name));
+
+ var gameDiv = document.createElement("button");
+ gameDiv.className = "duel-button";
+ gameDiv.onclick = () => onDuelButtonClicked(game);
+
+ // Change background color based on state
+ if (state === 'STOPPED') {
+ gameDiv.classList.add('duel-button-stopped');
+ } else if (state === 'INVITED') {
+ gameDiv.classList.add('duel-button-invited');
+ } else if (state === 'RUNNING') {
+ gameDiv.classList.add('duel-button-running');
+ } else if (state === 'WAITING') {
+ gameDiv.classList.add('duel-button-waiting');
+ } else if (state === 'WON') {
+ gameDiv.classList.add('duel-button-won');
+ } else if (state === 'LOST') {
+ gameDiv.classList.add('duel-button-lost');
+ }
+ // Create Icon for duel
+ const img = document.createElement("img");
+ if (gameName === "BSH") {
+ img.src = "./games/dpi24-06-battleship/battleship.svg";
+ } else {
+ // other game icons
+ img.src = "./img/cancel.svg";
+ }
+ img.alt = `Duel Image`;
+ img.className = "duel-image";
+ gameDiv.appendChild(img);
+
+ // Create text for duel button
+ const span = document.createElement("span");
+ span.className = "duel-text";
+ span.innerHTML = `Owner: ${ownerName} Participant: ${participantName} Start Time: ${startTime} State: ${state}`;
+
+ gameDiv.appendChild(span);
+ container.appendChild(gameDiv);
+ });
+ }
+}
+
+/**
+* Triggered when you click on an instance in duels overview.
+*/
+function onDuelButtonClicked(duelString) {
+ console.log("Button clicked for: " + JSON.stringify(duelString));
+ console.log("myId: ", JSON.stringify(myId));
+ var duelList = duelString.split(" ");
+ game = duelList[0]
+ console.log("owner: ", JSON.stringify(duelList[1]));
+ battleship_timestamp = duelList[3]
+ battleship_status = duelList[4]
+ switch (battleship_status) {
+ case "STOPPED":
+ return;
+ case "INVITED":
+ if (duelList[1] != myId) { // check if I am not the owner
+ // I am not owner
+ backend("games BSH INVACC " + duelList[1] + " " + myId); // nicht peerId
+ // TODO possibly add cooldown
+ } else {
+ // TODO open game to see ships
+ owner = duelList[1];
+ peer = "-"
+ battleships(null, duelList[6]);
+ }
+ return;
+ case "WON": // 6 = shotsDeliverOutcome, 7 = shotsReceivedOutcome, 8 = ships
+ owner = duelList[1];
+ peer = duelList[2];
+ battleships(null, duelList[6]);
+ return;
+ case "LOST":
+ owner = duelList[1];
+ peer = duelList[2];
+ battleships(null, duelList[6]);
+ return;
+ case "WAITING":
+ owner = duelList[1];
+ peer = duelList[2];
+ battleships(false, duelList[6]);
+ return;
+ case "RUNNING":
+ owner = duelList[1];
+ peer = duelList[2];
+ if (duelList[5] == "0") {
+ battleships(false, duelList[6]);
+ } else {
+ battleships(true, duelList[6]);
+ }
+ return;
+ case "SPEC":
+ owner = duelList[1];
+ peer = duelList[2];
+ battleships(null, duelList[6]);
+ return;
+ default:
+ return;
+ }
+}
+
+// eof
diff --git a/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/placeholder.jpg b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/placeholder.jpg
new file mode 100644
index 0000000..88109f8
Binary files /dev/null and b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/placeholder.jpg differ
diff --git a/android/tinySSB/app/src/main/assets/web/games/dpi24-09-connect4/connect4.css b/android/tinySSB/app/src/main/assets/web/games/dpi24-09-connect4/connect4.css
new file mode 100644
index 0000000..30b13aa
--- /dev/null
+++ b/android/tinySSB/app/src/main/assets/web/games/dpi24-09-connect4/connect4.css
@@ -0,0 +1,42 @@
+
+/* --------------------------------------------------------------------------- */
+/* Connect 4 */
+/* --------------------------------------------------------------------------- */
+
+#connect4-game-board {
+ height: 40vh;
+ padding: 10px;
+ margin-right: 15px;
+
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ grid-template-rows: repeat(6, 1fr);
+ grid-column-gap: 5px;
+ grid-row-gap: 5px;
+
+ background-color: blue;
+ border: 2px solid gold;
+ border-radius: 10px;
+}
+
+.connect4-game_tile {
+ background-color: white;
+ border-radius: 50%;
+ border: 5px solid;
+}
+
+#connect4-game-end-button {
+ font-size: 1.5em;
+ padding: 0.5em;
+ width: 5em;
+}
+
+#connect4-game-leave-button {
+ font-size: 1.5em;
+ padding: 0.5em;
+ width: 5em;
+}
+
+/* eof */
+
+
diff --git a/android/tinySSB/app/src/main/assets/web/games/dpi24-09-connect4/connect4.js b/android/tinySSB/app/src/main/assets/web/games/dpi24-09-connect4/connect4.js
new file mode 100644
index 0000000..860ec0a
--- /dev/null
+++ b/android/tinySSB/app/src/main/assets/web/games/dpi24-09-connect4/connect4.js
@@ -0,0 +1,526 @@
+// games/dpi24-09-connect4/connect4.js
+
+// Game board size
+const CONNECT4_GAME_COLUMNS = 7;
+const CONNECT4_GAME_ROWS = 6;
+
+/**
+ * Sets up the player selection menu for starting a new game.
+ *
+ */
+function connect4_menu_game_players() {
+ connect4_fill_players();
+ prev_scenario = 'connect4-game';
+ setScenario("connect4-game-players");
+ document.getElementById("div:textarea").style.display = 'none';
+ document.getElementById("div:connect4-confirm-player").style.display = 'flex';
+ document.getElementById("tremolaTitle").style.display = 'none';
+ var c = document.getElementById("conversationTitle");
+ c.style.display = null;
+ c.innerHTML = "Create New Game Select contact to play";
+ document.getElementById('plus').style.display = 'none';
+ closeOverlay();
+}
+/**
+ * Populates the player section menu with a list of available contacts.
+ *
+ */
+function connect4_fill_players() {
+ var choices = '';
+ for (var m in tremola.contacts) {
+ choices += '\n';
+ }
+ document.getElementById('lst:connect4-players').innerHTML = choices
+ document.getElementById(myId).disabled = true;
+}
+/**
+ * Sends invitation to the selected user to start a new game.
+ *
+ */
+function connect4_send_invite() {
+ myShortId = id2b32(myId);
+ var opponent = {}
+ for (var m in tremola.contacts) {
+ if (document.getElementById(m).checked) {
+ opponent = m;
+ }
+ }
+ var opponentShort = id2b32(opponent); // opponent.substring(0,11);
+ document.getElementById("div:connect4-confirm-player").style.display = 'none';
+ setScenario('connect4-game'); // UI change
+ persist();
+ backend(`connect_four_invite ${myShortId} ${opponentShort}`);
+}
+
+/**
+ * Clear all ongoing games within lst:connect4-games and builds the game button
+ * for each ongoing game.
+ */
+function connect4_load_games_list() {
+ document.getElementById("lst:connect4-games").innerHTML = '';
+ for (let gameId in tremola.game_connect4) {
+ connect4_build_game_item([gameId, tremola.game_connect4[gameId]]);
+ }
+}
+
+/**
+ * Create game button to resume ongoing game.
+ *@param game an Array that contains the GameID and a list with information about the game, in the format:
+ * [ id, { "alias": "player1 vs player2", "moves": {}, members: [] } ] }
+ */
+function connect4_build_game_item(game) { // [ id, { "alias": "player1 vs player2", "moves": {}, members: [] } ] }
+ var row, item = document.createElement('div'), bg;
+ item.setAttribute('style', 'padding: 0px 5px 10px 5px;'); // old JS (SDK 23)
+ if (!game[1].members.includes(myShortId)) {
+ row = "";
+ row += "" + escapeHTML(game[1].alias) + "
";
+ row += "
" + game[0] + "
";
+
+ item.innerHTML = row;
+ document.getElementById('lst:connect4-games').appendChild(item);
+ return;
+ }
+
+ row = "";
+ row += "" + escapeHTML(game[1].alias) + "
";
+ row += "
" + game[0] + "
";
+
+ item.innerHTML = row;
+ document.getElementById('lst:connect4-games').appendChild(item);
+}
+
+/**
+ * Is called after a new turn is received via the backend.
+ * Based on the playerToMove it assigns all board tiles to their
+ * respective owner and gives over to connect4_populate_game().
+ */
+function connect4_game_new_event(e) {
+ console.log("c4 new event " + JSON.stringify(e))
+ //fields
+ const gameId = e.public[1];
+ const playerToMove = e.public[2];
+ const members = e.public[3].split(',');
+ const stonePos = parseInt(e.public[4], 10);
+
+ let board;
+ //Case if user is not a member of the played game
+ //User creates new game, such that user has a list of all parallel games, but is not able to open it.
+ if (!(members.includes(myShortId))) {
+ if (tremola.game_connect4[gameId] && tremola.game_connect4[gameId].board) {
+ board = tremola.game_connect4[gameId].board; // Use the existing board, if game already exists in users tremola
+ } else {
+ board = Array.from(new Array(7), () => Array.from(new Array(6), () => ({}))); // Initialize a new board
+ }
+ tremola.game_connect4[gameId] = {
+ board: board,
+ members: members,
+ currentPlayer: playerToMove,
+ alias: fid2display(shortToFidMap[members[0]]) + " vs " + fid2display(shortToFidMap[members[1]]),
+ gameOver: false
+ };
+ connect4_load_games_list();
+ return;
+ }
+ //Case if User is in members list.
+ let idlePlayer = members.find(member => member != playerToMove);
+
+ if (tremola.game_connect4[gameId] && tremola.game_connect4[gameId].board) {
+ board = tremola.game_connect4[gameId].board; // Use the existing board
+ } else {
+ board = Array.from(new Array(7), () => Array.from(new Array(6), () => ({}))); // Initialize a new board
+ }
+ // Calculating position of the placed stone.
+ // Board is divided into 42 elements.
+ // top left of the board starts at 0. Bottom right ends with 41.
+ // when dividing position int with 7, we get the row. the rest is the column.
+ if(stonePos != -1) {
+ const x = stonePos % 7;
+ const y = Math.floor(stonePos / 7);
+ tremola.game_connect4[gameId].board[x][y].owner = idlePlayer;
+ }
+
+ const opponent = playerToMove == myShortId ? idlePlayer : playerToMove;
+
+ tremola.game_connect4[gameId] = {
+ board: board,
+ members: members,
+ currentPlayer: playerToMove,
+ alias: fid2display(shortToFidMap[opponent]) + " vs " + fid2display(myId),
+ gameOver: false
+ };
+ persist();
+
+ connect4_populate_game(gameId);
+ connect4_load_games_list();
+}
+
+/**
+ * This is received when the game is over, either if someone
+ * won or a player gave up. It updates the up and marks the
+ * game as over in the store.
+ *
+ */
+function connect4_game_end_event(e) {
+ console.log("c4 gae" + e + ' ' + myShortId)
+ const gameId = e.public[1];
+ const loser = e.public[2];
+ const stonePos = e.public[3];
+ // Checking if the user ID is in the gameID (Or more concrete: checking if user is a active game member)
+ //This check needs to be done, such that the user doesn't give up all games or doesn't win if a user from another game gives up
+ if (gameId.substring(0, 11) != myShortId && gameId.substring(11, 22) != myShortId) {
+ tremola.game_connect4[gameId].gameOver = true;
+ delete tremola.game_connect4[gameId];
+ connect4_load_games_list();
+ return;
+ }
+ // Calculation stone position of the last placed winning stone.
+ if(stonePos != -1) {
+ const x = stonePos % 7;
+ const y = Math.floor(stonePos / 7);
+ // Checking who lost.
+ if (loser == myShortId) {
+ if(gameId.substring(0,11) == myShortId) {
+ tremola.game_connect4[gameId].board[x][y].owner = gameId.substring(11, 22);
+ } else {
+ tremola.game_connect4[gameId].board[x][y].owner = gameId.substring(0, 11);
+ }
+ } else {
+ tremola.game_connect4[gameId].board[x][y].owner = myShortId;
+ }
+ connect4_populate_game(gameId);
+ connect4_load_games_list();
+ }
+
+ tremola.game_connect4[gameId].gameOver = true;
+ persist();
+
+ if (loser != myShortId) {
+ document.getElementById("connect4-game-turn-indicator").innerHTML = "You WON!";
+ } else {
+ document.getElementById("connect4-game-turn-indicator").innerHTML = "You LOST!";
+ }
+
+ document.getElementById("connect4-game-end-button").style.display = `none`;
+ setTimeout(showEndButton, 4000);
+ connect4_load_games_list();
+}
+
+/**
+ * Sets game-session scenario, title and button.
+ * Gives over to connect4_populate_game() afterwards.
+ */
+function connect4_open_game_session(gameId) {
+ setScenario('connect4-game-session');
+
+ document.getElementById("connect4-game-session-title").innerHTML = tremola.game_connect4[gameId].alias;
+ document.getElementById("connect4-game-end-button").onclick = () => connect4_end_game(gameId);
+ document.getElementById("connect4-game-leave-button").onclick = () => connect4_leave_game(gameId);
+ connect4_set_turn_indicator(gameId);
+
+ connect4_populate_game(gameId);
+}
+
+/**
+ * Creates the board and all the clickable tile elements.
+ * Color of tiles is given based on owner of the tile.
+ * Can also be used when game is not shown currently.
+ */
+function connect4_populate_game(gameId) {
+ document.getElementById('connect4-game-board').innerHTML = '';
+
+ const { board, members } = tremola.game_connect4[gameId];
+ const opponent = members.find(member => member != myShortId);
+
+ for (let y = 0; y < CONNECT4_GAME_ROWS; y++) {
+ for (let x = 0; x < CONNECT4_GAME_COLUMNS; x++) {
+ let tile = document.createElement('div');
+ tile.className = 'connect4-game_tile';
+ tile.onclick = () => connect4_add_stone(gameId, x);
+
+ const { owner } = board[x][y];
+ if (owner == myShortId) {
+ tile.style.backgroundColor = "yellow";
+ } else if (owner != myShortId && owner != null) {
+ tile.style.backgroundColor = "red";
+ }
+
+ document.getElementById('connect4-game-board').appendChild(tile);
+ board[x][y].tile = tile;
+
+ }
+ }
+
+ connect4_set_turn_indicator(gameId);
+}
+
+/**
+ * Tries to add a playing stone to the game field.
+ * After the stone is placed, checks if game is over and if so,
+ * informs the backend. If not gives over to connect4_end_turn().
+ */
+function connect4_add_stone(gameId, column) {
+ const { board, currentPlayer, members, gameOver } = tremola.game_connect4[gameId];
+ // Checking if user is in received game. if not, then user ignores game update message.
+ if (!(members.includes(myShortId))) {
+ return;
+ }
+ // Checking if it is users turn. else user can't place a stone, since it is not the users turn.
+ if (currentPlayer != myShortId || gameOver) {
+ return;
+ }
+ // Placing the stone. FreeSlots is the number of free spaces in the collumn. So as long as there are free spaces,
+ // the user can place a stone. Then the FreeSlots gets decremented, such that the stone is in the lowest possible position.
+ const freeSlots = board[column].filter(t => t.owner == null).length;
+ if (freeSlots > 0) {
+ const boardElement = board[column][freeSlots - 1];
+ // Calculating stone position as an integer between 0-41.
+ const stonePos = ((freeSlots - 1) * 7) + column
+ boardElement.owner = myShortId;
+ boardElement.tile.style.backgroundColor = "yellow";
+
+ const gameover = connect4_check_gameover(gameId);
+ // Checking if game is over.
+ if (gameover) {
+ const loser = members.find(member => member != myShortId);
+ tremola.game_connect4[gameId].gameOver = true;
+ persist();
+ backend(`connect_four_end ${gameId} ${loser} ${stonePos}`);
+ return;
+ }
+
+ connect4_end_turn(gameId, stonePos);
+ }
+}
+
+/**
+ * Checks if game is over by checking if stones align so
+ * that 4 stones are adjacent to each other.
+ * @param gameId Id of the game that is responded
+ */
+function connect4_check_gameover(gameId) {
+ const { board: b } = tremola.game_connect4[gameId];
+
+ // Check down
+ for (let y = 0; y < 3; y++)
+ for (let x = 0; x < 7; x++)
+ if (connect4_check_line(b[x][y], b[x][y+1], b[x][y+2], b[x][y+3]))
+ return true;
+
+ // Check right
+ for (let y = 0; y < 6; y++)
+ for (let x = 0; x < 4; x++)
+ if (connect4_check_line(b[x][y], b[x+1][y], b[x+2][y], b[x+3][y]))
+ return true;
+
+ // Check down-right
+ for (let y = 0; y < 3; y++)
+ for (let x = 0; x < 4; x++)
+ if (connect4_check_line(b[x][y], b[x+1][y+1], b[x+2][y+2], b[x+3][y+3]))
+ return true;
+
+ // Check down-left
+ for (let y = 3; y < 6; y++)
+ for (let x = 0; x < 4; x++)
+ if (connect4_check_line(b[x][y], b[x+1][y-1], b[x+2][y-2], b[x+3][y-3]))
+ return true;
+
+ return false;
+}
+
+/**
+ * Helper function for connect4_check_gameover() to check if 4 stones
+ * are adjacent.
+ * @param a first stone to be checked
+ * @param b second stone to be checked
+ * @param c third stone to be checked
+ * @param d fourth stone to be checked
+ */
+function connect4_check_line(a, b, c, d) {
+ // Check first cell non-zero and all cells match
+ return a.owner != null &&
+ a.owner == b.owner &&
+ a.owner == c.owner &&
+ a.owner == d.owner;
+}
+
+/**
+ * Sets the new currentPlayer to the store and updates
+ * the UI accordingly with the turn indicator.
+ * Sends board information after turn is over via backend.
+ * @param gameId ID of the responsable game
+ * @param stonePos position of the stone on the board
+ */
+function connect4_end_turn(gameId, stonePos) {
+ const { currentPlayer } = tremola.game_connect4[gameId];
+ const opponent = tremola.game_connect4[gameId].members.find(member => member != myShortId);
+
+ if (currentPlayer == myShortId) {
+ tremola.game_connect4[gameId].currentPlayer = opponent;
+ } else {
+ tremola.game_connect4[gameId].currentPlayer = myShortId;
+ }
+ persist();
+ connect4_set_turn_indicator(gameId);
+ connect4_send_board(gameId, stonePos);
+}
+
+/**
+ * Sends the position of the new positioned Stone on the board via backend
+ * @param gameId ID of the responsable game
+ * @param stonePos Position of the stone that was placed
+ */
+function connect4_send_board(gameId, stonePos) {
+ const { board, currentPlayer: playerToMove } = tremola.game_connect4[gameId];
+
+ const { members } = tremola.game_connect4[gameId];
+
+ backend(`connect_four ${gameId} ${playerToMove} ${members.join(',')} ${stonePos}`);
+}
+
+/**
+ * Sets UI turn indicator according to currentPlayer.
+ * @param gameId ID of the responsable game
+ */
+function connect4_set_turn_indicator(gameId) {
+ if (tremola.game_connect4[gameId].currentPlayer == myShortId) {
+ document.getElementById("connect4-game-turn-indicator").innerHTML = "Your turn!";
+ } else {
+ document.getElementById("connect4-game-turn-indicator").innerHTML = "Wait for your opponent.";
+ }
+}
+
+/**
+ * Ends game, either by Giving up or if game is over. After 4 seconds it shows a button to leave the session
+ * @param gameId ID of the responsable game
+ */
+function connect4_end_game(gameId) {
+ document.getElementById("connect4-game-end-button").innerHTML = "Give up";
+ document.getElementById("connect4-game-turn-indicator").innerHTML = "You LOST!";
+ document.getElementById("connect4-game-end-button").style.display = `none`;
+ backend(`connect_four_end ${gameId} ${myShortId} ${-1}`);
+ //Adds a delay for the button to be showed to ensure proper data transmission without errors
+ setTimeout(showEndButton, 4000);
+}
+
+/**
+ * Leaves the game after it is finished, via button click, and also deletes it from the database
+ * @param gameId ID of the Game that is leaved
+ */
+function connect4_leave_game(gameId) {
+ document.getElementById("connect4-game-end-button").style.display = `block`;
+ document.getElementById("connect4-game-leave-button").style.display = `none`;
+ setScenario("connect4-game");
+ persist();
+ delete tremola.game_connect4[gameId];
+ connect4_load_games_list();
+}
+
+/**
+ * Receives an invitation message and if this invite was meant for the player then it shows the invite PupUp, else it is ignored
+ * @param e invitation event
+ */
+function connect4_recv_invite(e) {
+ console.log("c4 rxi " + e + ' ' + myShortId)
+ const inviterShort = e.public[1];
+ const invitedShort = e.public[2];
+
+ if (myShortId == invitedShort) {
+ showInvitePopup(inviterShort);
+ }
+
+}
+
+/**
+ * Shows the end Button when a game is Finished
+ */
+function showEndButton() {
+ document.getElementById('connect4-game-leave-button').style.display = 'block';
+}
+
+/**
+ * Shows an invitation notification in form of a popUp, whenever a user gets invited to a game by another user.
+ * This Popup contains of a decline and accept button
+ * @param inviterShort The shorted ID of the inviter
+ */
+function showInvitePopup(inviterShort) {
+ document.getElementById('connect4-game-invite-popup').style.display = 'block';
+ inviterLong = shortToFidMap[inviterShort];
+ inviterAlias = fid2display(inviterLong);
+
+ document.getElementById('connect4-invite-message').innerText = `You have been invited by ${inviterAlias} to play a game of connect four.`;
+ document.getElementById('connect4-game-invite-popup').dataset.inviterShort = inviterShort;
+}
+
+/**
+ * Hides the invite popup when a user declines or accpts it.
+ */
+function hideInvitePopup() {
+ document.getElementById('connect4-game-invite-popup').style.display = 'none';
+}
+
+/**
+ * Accepts the invite and starts game. It also sends a message that the game is created now and hides the popup after that
+ */
+
+function acceptInvite() {
+ inviterShort = document.getElementById('connect4-game-invite-popup').dataset.inviterShort;
+ players = [myShortId, inviterShort];
+ gameId = recps2nm(players);
+ hideInvitePopup();
+
+ if (tremola.game_connect4 == null) {
+ tremola.game_connect4 = {};
+ }
+
+ if (!(gameId in tremola.game_connect4)) {
+ tremola.game_connect4[gameId] = {
+ alias: fid2display(shortToFidMap[inviterShort]) + " vs " + fid2display(myId),
+ board: Array.from(new Array(7), () => Array.from(new Array(6), () => ({}))),
+ currentPlayer: inviterShort,
+ members: players,
+ gameOver: false
+ };
+ }
+
+ document.getElementById("div:connect4-confirm-player").style.display = 'none';
+ connect4_open_game_session(gameId);
+
+ persist();
+ connect4_send_board(gameId, -1);
+}
+
+/**
+ * Declines the invitation and sends the inviter a message that the invitee declined the invite
+ */
+function declineInvite() {
+ inviterShort = document.getElementById('connect4-game-invite-popup').dataset.inviterShort;
+ hideInvitePopup();
+ persist();
+ backend(`connect_four_decline_invite ${inviterShort} ${myShortId}`)
+}
+
+/**
+ * Hides the decline information popup when user clicks on the button
+ */
+function hideDeclinePopup() {
+ document.getElementById("connect4-game-decline-invite-popup").style.display = 'none';
+}
+
+/**
+ * Declines the invitation and sents the inviter an information message about the decline
+ * @param e buttonclick event
+ */
+function connect4_invite_declined(e) {
+ const inviterShort = e.public[1];
+ const invitedShort = e.public[2];
+
+ const invitedAlias = fid2display(shortToFidMap[invitedShort]);
+ if(myShortId == inviterShort) {
+ document.getElementById("connect4-game-decline-invite-popup").style.display = 'block';
+ document.getElementById('connect4-decline-invite-message').innerText = `Your invitation to ${invitedAlias} has been declined.`;
+ }
+}
diff --git a/android/tinySSB/app/src/main/assets/web/games/games.js b/android/tinySSB/app/src/main/assets/web/games/games.js
index 976c95d..06fccbe 100644
--- a/android/tinySSB/app/src/main/assets/web/games/games.js
+++ b/android/tinySSB/app/src/main/assets/web/games/games.js
@@ -4,28 +4,32 @@
function load_game_list() {
document.getElementById("lst:games").innerHTML = '';
- load_game_item('Battleship (dpi24.06)', 'games/dpi24-06-battelship/battleship.svg',
+ load_game_item('Battleship (dpi24.06)', 'games/dpi24-06-battleship/battleship.svg', 'show_duels()',
+ "Strategy game for two, played on ruled grids on which each player's fleet of warships (randomly generated) are positioned. Authors: Mirco Franco and Lars Schneider");
+ /* excluded because chrome emulator-only
+ load_game_item('Snake (dpi24.07)', 'games/dpi24-07-snake/snake.png', '',
'text text text h aghjwd gldfhjs hlgsf hgljksf hgls fdhglf sdhgl hfgskj hls dfhgjl shgjkls hgl sfdhgjk sdfjklg hljs hfgl dfjlsfs');
- load_game_item('Snake (dpi24.07)', 'games/dpi24-07-snake/snake.png',
+ */
+ /* excluded because it used the old tremola code based instead of tinySSB
+ load_game_item('Checkers (dpi24.08)', 'games/dpi24-08-checkers/checkers.svg', '',
'text text text h aghjwd gldfhjs hlgsf hgljksf hgls fdhglf sdhgl hfgskj hls dfhgjl shgjkls hgl sfdhgjk sdfjklg hljs hfgl dfjlsfs');
- load_game_item('Checkers (dpi24.08)', 'games/dpi24-08-checkers/checkers.svg',
+ */
+ load_game_item('Connect4 (dpi24.09)', 'games/dpi24-09-connect4/connect4.png', 'setScenario("connect4-game")',
'text text text h aghjwd gldfhjs hlgsf hgljksf hgls fdhglf sdhgl hfgskj hls dfhgjl shgjkls hgl sfdhgjk sdfjklg hljs hfgl dfjlsfs');
- load_game_item('Connect4 (dpi24.09)', 'games/dpi24-09-connect4/connect4.png',
- 'text text text h aghjwd gldfhjs hlgsf hgljksf hgls fdhglf sdhgl hfgskj hls dfhgjl shgjkls hgl sfdhgjk sdfjklg hljs hfgl dfjlsfs');
- load_game_item('Blackjack (dpi24.10)', 'games/dpi24-10-blackjack/coins.svg',
+ load_game_item('Blackjack (dpi24.10)', 'games/dpi24-10-blackjack/coins.svg', '',
'crypto tokens based on CRDTs, no mining needed. Ideal for fidelity cards, bartering, recognition tokens in open SW communities, and more.');
- load_game_item('Hangman (dpi24.11)', 'games/dpi24-11-hangman/hangman.svg',
+ load_game_item('Hangman (dpi24.11)', 'games/dpi24-11-hangman/hangman.svg', '',
'dah dah dah');
}
-function load_game_item(title, imageName, descr) {
+function load_game_item(title, imageName, fct, descr) {
var row, item = document.createElement('div'), bg;
item.setAttribute('style', 'padding: 0px 5px 10px 5px;'); // old JS (SDK 23)
bg = ' light'; // c[1].forgotten ? ' gray' : ' light';
row = ` `;
- row += "";
+ row += ``;
row += "" + escapeHTML(title) + "
";
- row += "
" + escapeHTML(descr) + " ";
+ row += "" + descr + " ";
item.innerHTML = row;
document.getElementById('lst:games').appendChild(item);
}
diff --git a/android/tinySSB/app/src/main/assets/web/prod/dpi24-14-sched/scheduling.css b/android/tinySSB/app/src/main/assets/web/prod/dpi24-14-sched/scheduling.css
new file mode 100644
index 0000000..1aa8619
--- /dev/null
+++ b/android/tinySSB/app/src/main/assets/web/prod/dpi24-14-sched/scheduling.css
@@ -0,0 +1,88 @@
+/*
+ prod/dpi24-14-sched/scheduling.css
+*/
+
+
+/* scheduling */
+
+.attendance-button {
+ padding: 5px 10px;
+ margin-right: 10px;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ transition: background-color 0.3s;
+}
+
+.attendance-button.attending {
+ background-color: #e0ffe0;
+ color: #006400;
+}
+
+.attendance-button.not-attending {
+ background-color: #ffe0e0;
+ color: #8b0000;
+}
+
+.attendance-button.active {
+ font-weight: bold;
+}
+
+.attendance-list {
+ font-size: 0.9em;
+ margin-top: 10px;
+}
+
+.appointment-item {
+ min-height: 150px; /* Increase the minimum height */
+ margin-bottom: 15px; /* Add some space between appointments */
+}
+
+.appointment-item .chat_item_button {
+ height: 100%; /* Make the button fill the entire height of the item */
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+}
+
+.appointment-item .attendance-buttons {
+ display: flex;
+ justify-content: space-around;
+ margin: 10px 0;
+}
+
+.appointment-item .attendance-list {
+ max-height: 100px; /* Limit the height of the attendance list */
+ overflow-y: auto; /* Add scrolling if the list is too long */
+}
+
+.delete-button {
+ background-color: #ff6b6b;
+ color: white;
+ border: none;
+ padding: 5px 10px;
+ border-radius: 5px;
+ cursor: pointer;
+ transition: background-color 0.3s;
+}
+
+.delete-button:hover {
+ background-color: #ff4757;
+}
+
+.delete-button {
+ background-color: #ff6b6b;
+ color: white;
+ border: none;
+ padding: 5px 10px;
+ border-radius: 5px;
+ cursor: pointer;
+ transition: background-color 0.3s;
+ margin-top: 10px;
+}
+
+.delete-button:hover {
+ background-color: #ff4757;
+}
+
+/* eof */
diff --git a/android/tinySSB/app/src/main/assets/web/prod/kanban/kanban.css b/android/tinySSB/app/src/main/assets/web/prod/kanban/kanban.css
new file mode 100644
index 0000000..8c63f57
--- /dev/null
+++ b/android/tinySSB/app/src/main/assets/web/prod/kanban/kanban.css
@@ -0,0 +1,200 @@
+/*
+ prod/kanban/kanban.css
+*/
+
+/* --------------------------------------------------------------------------- */
+/* Kanban Board */
+/* --------------------------------------------------------------------------- */
+
+.board_item_button {
+ border: none;
+ text-align: left;
+ vertical-align: top;
+ height: 3em;
+ font-size: medium;
+ border-radius: 4pt;
+ box-shadow: 0 0 5px rgba(0,0,0,0.7);
+ /*background-color: #c1e1c1;*/
+}
+
+.columns_container {
+ position: relative;
+ width: 100%;
+ /*height: 100%;*/
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ gap: 10px;
+}
+
+.column {
+ display: flex;
+ justify-content: flex-start;
+ flex-direction: column;
+ border-radius: 5pt;
+}
+
+.column_wrapper{
+ flex: 0 0 36vw;
+ width: 36vw;
+}
+
+.column_content{
+ display: flex;
+ justify-content: flex-start;
+ flex-direction: column;
+ gap: 10px;
+ padding-bottom: 10px;
+ padding-top: 10px;
+}
+
+.column_hdr {
+ width: 100%;
+ margin: 0 auto;
+ padding-top:5px;
+ padding-bottom:5px;
+ overflow-wrap: break-word;
+ background-color: #c1e1c1;
+ border-bottom-color: gray;
+ border-bottom-style: solid;
+ border-bottom-width: 2px;
+ border-radius: 5pt;
+}
+
+/*
+.column_options{
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+*/
+
+.context_options_btn {
+ border: none;
+ text-align: left;
+ height: 3em;
+ width: 100%;
+ font-size: medium;
+ background-color: white;
+}
+
+
+.context_menu {
+ display: none;
+ width: 10%;
+ max-height: 80vh;
+ position: absolute;
+ background-color: #f9f9f9;
+ min-width: 160px;
+ box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
+ z-index: 1001;
+ overflow-x: hidden;
+ overflow-y:scroll;
+}
+
+.column_item {
+ width:95%;
+ margin: auto;
+ font-size: medium;
+ border-radius: 4pt;
+ box-shadow: 0 0 5px rgb(0 0 0 / 70%);
+ min-height: 3em;
+}
+
+.item_button {
+ width:100%;
+ border: none;
+ text-align: left;
+ vertical-align: top;
+ font-size: medium;
+ border-radius: 4pt;
+ box-shadow: 0 0 5px rgba(0,0,0,0.7);
+ white-space: normal;
+ word-wrap: break-word;
+ min-height: 3em;
+}
+
+.item_menu_content {
+ padding-top: 10px;
+ float: left;
+ width: 70%;
+}
+
+.item_menu_desc {
+ margin-top: 10px;
+ border: none;
+ outline: none;
+ background-color: rgb(211,211,211);
+}
+
+.item_menu_buttons{
+ padding-top: 10px;
+ float: right;
+ width: 30%;
+}
+
+.item_menu_button {
+ width: 90%;
+ float: right;
+ margin:5px auto;
+ display:block;
+}
+
+.div:item_menu_assignees_container {
+ padding-top: 5px;
+ width: 100%;
+ display: flex;
+ justify-content: flex-start;
+ flex-direction: column;
+}
+
+.column_footer {
+ width: 100%;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ border-top-color: gray;
+ border-top-style: solid;
+ border-top-width: 2px;
+}
+
+
+.kanban_invitation_container {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: 1fr;
+ gap: 0px 0px;
+ grid-template-areas:
+ "text btns";
+ width:100%;
+ box-shadow: 0 0 5px rgba(0,0,0,0.7);
+ border-radius: 4pt;
+ height: 3em;
+ margin-top: 5px;
+}
+
+.kanban_invitation_text_container {
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-template-rows: 1fr 1fr;
+ gap: 0px 0px;
+ grid-template-areas:
+ "name"
+ "author";
+ grid-area: text;
+}
+
+.kanban_create_personal_btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 16px;
+ margin: 0 10px;
+ background-color: #51a4d2;
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+ height: 40px;
+ width: 35px
+}
+
+/* eof */
\ No newline at end of file
diff --git a/android/tinySSB/app/src/main/assets/web/tremola.css b/android/tinySSB/app/src/main/assets/web/tremola.css
index 388c02c..27a6d07 100644
--- a/android/tinySSB/app/src/main/assets/web/tremola.css
+++ b/android/tinySSB/app/src/main/assets/web/tremola.css
@@ -516,281 +516,4 @@ input:checked + .slider:before {
filter: invert(62%) sepia(8%) saturate(116%) hue-rotate(145deg) brightness(89%) contrast(89%);
}
-/* --------------------------------------------------------------------------- */
-/* Kanban Board */
-/* --------------------------------------------------------------------------- */
-
-.board_item_button {
- border: none;
- text-align: left;
- vertical-align: top;
- height: 3em;
- font-size: medium;
- border-radius: 4pt;
- box-shadow: 0 0 5px rgba(0,0,0,0.7);
- /*background-color: #c1e1c1;*/
-}
-
-.columns_container {
- position: relative;
- width: 100%;
- /*height: 100%;*/
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- gap: 10px;
-}
-
-.column {
- display: flex;
- justify-content: flex-start;
- flex-direction: column;
- border-radius: 5pt;
-}
-
-.column_wrapper{
- flex: 0 0 36vw;
- width: 36vw;
-}
-
-.column_content{
- display: flex;
- justify-content: flex-start;
- flex-direction: column;
- gap: 10px;
- padding-bottom: 10px;
- padding-top: 10px;
-}
-
-.column_hdr {
- width: 100%;
- margin: 0 auto;
- padding-top:5px;
- padding-bottom:5px;
- overflow-wrap: break-word;
- background-color: #c1e1c1;
- border-bottom-color: gray;
- border-bottom-style: solid;
- border-bottom-width: 2px;
- border-radius: 5pt;
-}
-
-/*
-.column_options{
- display: flex;
- flex-direction: row;
- justify-content: space-between;
-}
-*/
-
-.context_options_btn {
- border: none;
- text-align: left;
- height: 3em;
- width: 100%;
- font-size: medium;
- background-color: white;
-}
-
-
-.context_menu {
- display: none;
- width: 10%;
- max-height: 80vh;
- position: absolute;
- background-color: #f9f9f9;
- min-width: 160px;
- box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
- z-index: 1001;
- overflow-x: hidden;
- overflow-y:scroll;
-}
-
-.column_item {
- width:95%;
- margin: auto;
- font-size: medium;
- border-radius: 4pt;
- box-shadow: 0 0 5px rgb(0 0 0 / 70%);
- min-height: 3em;
-}
-
-.item_button {
- width:100%;
- border: none;
- text-align: left;
- vertical-align: top;
- font-size: medium;
- border-radius: 4pt;
- box-shadow: 0 0 5px rgba(0,0,0,0.7);
- white-space: normal;
- word-wrap: break-word;
- min-height: 3em;
-}
-
-.item_menu_content {
- padding-top: 10px;
- float: left;
- width: 70%;
-}
-
-.item_menu_desc {
- margin-top: 10px;
- border: none;
- outline: none;
- background-color: rgb(211,211,211);
-}
-
-.item_menu_buttons{
- padding-top: 10px;
- float: right;
- width: 30%;
-}
-
-.item_menu_button {
- width: 90%;
- float: right;
- margin:5px auto;
- display:block;
-}
-
-.div:item_menu_assignees_container {
- padding-top: 5px;
- width: 100%;
- display: flex;
- justify-content: flex-start;
- flex-direction: column;
-}
-
-.column_footer {
- width: 100%;
- padding-top: 5px;
- padding-bottom: 5px;
- border-top-color: gray;
- border-top-style: solid;
- border-top-width: 2px;
-}
-
-
-.kanban_invitation_container {
- display: grid;
- grid-template-columns: 1fr 1fr;
- grid-template-rows: 1fr;
- gap: 0px 0px;
- grid-template-areas:
- "text btns";
- width:100%;
- box-shadow: 0 0 5px rgba(0,0,0,0.7);
- border-radius: 4pt;
- height: 3em;
- margin-top: 5px;
-}
-
-.kanban_invitation_text_container {
- display: grid;
- grid-template-columns: 1fr;
- grid-template-rows: 1fr 1fr;
- gap: 0px 0px;
- grid-template-areas:
- "name"
- "author";
- grid-area: text;
-}
-
-.kanban_create_personal_btn {
- background: none;
- border: none;
- cursor: pointer;
- font-size: 16px;
- margin: 0 10px;
- background-color: #51a4d2;
- background-size: contain;
- background-repeat: no-repeat;
- background-position: center;
- height: 40px;
- width: 35px
-}
-
-/* scheduling */
-
-.attendance-button {
- padding: 5px 10px;
- margin-right: 10px;
- border: none;
- border-radius: 5px;
- cursor: pointer;
- transition: background-color 0.3s;
-}
-
-.attendance-button.attending {
- background-color: #e0ffe0;
- color: #006400;
-}
-
-.attendance-button.not-attending {
- background-color: #ffe0e0;
- color: #8b0000;
-}
-
-.attendance-button.active {
- font-weight: bold;
-}
-
-.attendance-list {
- font-size: 0.9em;
- margin-top: 10px;
-}
-
-.appointment-item {
- min-height: 150px; /* Increase the minimum height */
- margin-bottom: 15px; /* Add some space between appointments */
-}
-
-.appointment-item .chat_item_button {
- height: 100%; /* Make the button fill the entire height of the item */
- display: flex;
- flex-direction: column;
- justify-content: space-between;
-}
-
-.appointment-item .attendance-buttons {
- display: flex;
- justify-content: space-around;
- margin: 10px 0;
-}
-
-.appointment-item .attendance-list {
- max-height: 100px; /* Limit the height of the attendance list */
- overflow-y: auto; /* Add scrolling if the list is too long */
-}
-
-.delete-button {
- background-color: #ff6b6b;
- color: white;
- border: none;
- padding: 5px 10px;
- border-radius: 5px;
- cursor: pointer;
- transition: background-color 0.3s;
-}
-
-.delete-button:hover {
- background-color: #ff4757;
-}
-
-.delete-button {
- background-color: #ff6b6b;
- color: white;
- border: none;
- padding: 5px 10px;
- border-radius: 5px;
- cursor: pointer;
- transition: background-color 0.3s;
- margin-top: 10px;
-}
-
-.delete-button:hover {
- background-color: #ff4757;
-}
-
/* eof */
diff --git a/android/tinySSB/app/src/main/assets/web/tremola.html b/android/tinySSB/app/src/main/assets/web/tremola.html
index f148823..81942b6 100644
--- a/android/tinySSB/app/src/main/assets/web/tremola.html
+++ b/android/tinySSB/app/src/main/assets/web/tremola.html
@@ -3,6 +3,10 @@
+
+
+
+
@@ -17,6 +21,8 @@
+
+
@@ -132,6 +138,43 @@
+
+
+
+
+
+
Invite
+
+
Waiting ...
+
+
Waiting for
+ the other player
+
+
+
Boat Length: 0
+
Horizontal
+
+
+
Your Turn
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
TEST
+
+ Give up
+ End!
+
+
+
+
+
+
+
+
+
+
diff --git a/android/tinySSB/app/src/main/assets/web/tremola.js b/android/tinySSB/app/src/main/assets/web/tremola.js
index 12dc06c..e17f2e1 100644
--- a/android/tinySSB/app/src/main/assets/web/tremola.js
+++ b/android/tinySSB/app/src/main/assets/web/tremola.js
@@ -4,6 +4,8 @@
var tremola;
var myId;
+var myShortId;
+var shortToFidMap = {}
var localPeers = {}; // feedID ~ [isOnline, isConnected] - TF, TT, FT - FF means to remove this entry
var must_redraw = false;
var edit_target = '';
@@ -460,6 +462,18 @@ function b2f_new_in_order_event(e) {
console.log("New kanban event")
kanban_new_event(e)
break
+ case "C4N":
+ connect4_game_new_event(e);
+ break
+ case "C4E":
+ connect4_game_end_event(e);
+ break
+ case "C4I":
+ connect4_recv_invite(e);
+ break
+ case "C4D":
+ connect4_invite_declined(e);
+ break
case "SCH":
console.log("New scheduling event")
dpi_scheduling_new_event(e)
@@ -601,6 +615,24 @@ function b2f_new_event(e) { // incoming SSB log event: we get map with three ent
}
}
+ } else if (e.public[0] == "GAM") {
+ // TODO autoretransmit answer if necessary
+ if (window.GamesHandler && typeof window.GamesHandler.onGameBackendEvent === 'function') {
+ var response = window.GamesHandler.onGameBackendEvent(e.public[1]);
+ var responseList = response.split("!CERBERUS!")
+ if (responseList.length >= 1 && responseList[0].startsWith("games")) {
+ // TODO anpassen von GUI
+ backend(responseList[0]);
+ }
+ if (responseList.length == 2) {
+ update_game_gui(responseList[1])
+ } else {
+ update_game_gui(responseList[0])
+ }
+ } else {
+ console.error("GamesHandler.onGameBackendEvent is not a function");
+ }
+ //Android.onGameBackendEvent(e.public);
}
persist();
must_redraw = true;
@@ -698,6 +730,7 @@ function b2f_new_image_blob(ref) {
function b2f_initialize(id, settings) {
myId = id
+ myShortId = id2b32(myId)
if (window.localStorage.tremola) {
tremola = JSON.parse(window.localStorage.getItem('tremola'));
@@ -722,7 +755,7 @@ function b2f_initialize(id, settings) {
load_prod_list()
load_game_list()
load_contact_list()
-
+ loadShortIds()
// load_kanban_list()
// dpi_load_event_list() // problem with access to tremola object
@@ -731,4 +764,14 @@ function b2f_initialize(id, settings) {
// load_chat("ALL");
}
+
+function addToMap(fid) {
+ const fidShort = id2b32(fid) // fid.substring(0, 7); // Extract the shortID from the longID
+ shortToFidMap[fidShort] = fid; // Map shortID to longID
+}
+function loadShortIds() {
+ for (var id in tremola.contacts)
+ addToMap(id);
+}
+
// --- eof
diff --git a/android/tinySSB/app/src/main/assets/web/tremola_ui.js b/android/tinySSB/app/src/main/assets/web/tremola_ui.js
index 24e67d8..1a4f0f9 100644
--- a/android/tinySSB/app/src/main/assets/web/tremola_ui.js
+++ b/android/tinySSB/app/src/main/assets/web/tremola_ui.js
@@ -8,12 +8,15 @@ var display_or_not = [
'div:qr', 'div:back', 'core', 'plus',
'lst:chats', 'lst:prod', 'lst:games', 'lst:contacts', 'lst:members',
'div:posts', 'lst:kanban', 'div:board',
+ 'lst:duels', 'div:battleships', // battleship
+ 'lst:connect4-game', 'lst:connect4-players', 'the:connect4-game-session', // connect4
'lst:scheduling', 'div:event',
'div:footer', 'div:textarea', 'div:confirm-members', 'div:settings'
];
var prev_scenario = 'chats';
var curr_scenario = 'chats';
+var game_scenario = false;
var scenarioDisplay = {
'chats': ['div:qr', 'core', 'lst:chats', 'div:footer', 'plus'],
@@ -23,8 +26,13 @@ var scenarioDisplay = {
'members': ['div:back', 'core', 'lst:members', 'div:confirm-members'],
'productivity': ['div:qr', 'core', 'lst:prod', 'div:footer'],
'settings': ['div:back', 'div:settings', 'core'],
- 'kanban': ['div:qr', 'core', 'lst:kanban', 'div:footer', 'plus'],
- 'board': ['div:back', 'core', 'div:board'],
+ 'kanban': ['div:qr', 'core', 'lst:kanban', 'div:footer', 'plus'], // KANBAN
+ 'board': ['div:back', 'core', 'div:board'], // KANBAN
+ 'duels': ['div:back', 'core', 'lst:duels', 'plus'], // BATTLESHIP
+ 'battleships': ['div:back', 'core', 'div:battleships'], // BATTLESHIP
+ 'connect4-game': ['div:back', 'core', 'lst:connect4-game', 'div:footer', 'plus'],
+ 'connect4-game-players': ['div:back', 'core', 'lst:connect4-players', 'div:connect4-confirm-player'],
+ 'connect4-game-session': ['div:back', 'core', 'the:connect4-game-session'],
'scheduling': ['div:qr', 'core', 'lst:scheduling', 'div:footer', 'plus'],
'event': ['div:back', 'core', 'div:event']
}
@@ -71,6 +79,13 @@ var scenarioMenu = {
['(un)Forget', 'board_toggle_forget'],
['Debug', 'ui_debug']],
+ 'duels': [], // BATTLESHIP
+ 'battleships': [['Quit Game', 'quit_bsh']], // BATTLESHIP
+
+ 'connect4-game': [// ['New Game'], //, 'connect4_menu_game_players'],
+ ['Settings', 'menu_settings'],
+ ['About', 'menu_about']],
+
'scheduling': [
// ['New Event', 'dpi_menu_new_event'],
['Invitations', 'dpi_menu_event_invitations'],
@@ -96,14 +111,21 @@ function onBackPressed() {
closeOverlay();
return;
}
- if (['chats', 'contacts', 'board', 'event'].indexOf(curr_scenario) >= 0) {
+ if (['chats', 'contacts', 'board', 'event', 'battleships',
+ 'connect4-game-session'].indexOf(curr_scenario) >= 0) {
if (curr_scenario == 'chats')
backend("onBackPressed");
else if (curr_scenario == 'board')
setScenario('kanban')
else if (curr_scenario == 'event')
setScenario('scheduling')
- else
+ else if (curr_scenario == 'battleships') { // BATTLESHIP // TODO prev_scenario für duels unc posts und nicht in chat
+ reset_battleship_mode()
+ show_duels()
+ } else if (curr_scenario == 'connect4-game-session') {
+ connect4_load_games_list();
+ setScenario('connect4-game')
+ } else
setScenario('chats')
} else {
if (curr_scenario == 'settings') {
@@ -137,14 +159,17 @@ function setScenario(s) {
}
})
// console.log('s: ' + s)
- if (s != "board") {
+ if (s != "board" && s != '') {
document.getElementById('tremolaTitle').style.position = null;
}
- if (s == "posts" || s == "settings" || s == "board" || s == "event") {
+ if (s == "posts" || s == "settings" || s == "board" || s == "event" ||
+ s == 'battleships') {
document.getElementById('tremolaTitle').style.display = 'none';
document.getElementById('conversationTitle').style.display = null;
document.getElementById('plus').style.display = 'none';
+ } else if (s == "duels") {
+ document.getElementById('tremolaTitle').style.display = 'none';
} else {
document.getElementById('tremolaTitle').style.display = null;
document.getElementById('conversationTitle').style.display = 'none';
@@ -199,6 +224,19 @@ function setScenario(s) {
}, 100);
}
+ if (s == 'connect4-game') {
+ document.getElementById("tremolaTitle").style.display = 'none';
+ var c = document.getElementById("conversationTitle");
+ c.style.display = null;
+ c.innerHTML = "Connect4 Sessions Pick or create a session";
+ connect4_load_games_list();
+ } else if (s == 'connect4-game-session') {
+ document.getElementById("tremolaTitle").style.display = 'none';
+ var c = document.getElementById("conversationTitle");
+ c.style.display = null;
+ c.innerHTML = "Connect4 ";
+ }
+
if (s == 'scheduling') {
document.getElementById("tremolaTitle").style.display = 'none';
var c = document.getElementById("conversationTitle");
@@ -348,6 +386,10 @@ function plus_button() {
menu_new_contact();
} else if (curr_scenario == 'kanban') {
menu_new_board();
+ } else if (curr_scenario == 'duels') {
+ inviteForDuel('BSH');
+ } else if (curr_scenario == 'connect4-game' ) {
+ connect4_menu_game_players();
} else if (curr_scenario == 'scheduling') {
dpi_menu_new_event();
}
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/MainActivity.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/MainActivity.kt
index 8c2d89f..1be86e1 100644
--- a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/MainActivity.kt
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/MainActivity.kt
@@ -27,6 +27,7 @@ import nz.scuttlebutt.tremolavossbol.tssb.ble.BlePeers
import nz.scuttlebutt.tremolavossbol.tssb.*
import nz.scuttlebutt.tremolavossbol.tssb.ble.BluetoothEventListener
import nz.scuttlebutt.tremolavossbol.utils.Constants
+import nz.scuttlebutt.tremolavossbol.games.common.GamesHandler
import tremolavossbol.R
import java.net.*
import java.util.concurrent.locks.ReentrantLock
@@ -40,6 +41,7 @@ class MainActivity : Activity() {
// lateinit var tremolaState: TremolaState
lateinit var idStore: IdStore
lateinit var wai: WebAppInterface
+ lateinit var gamesHandler: GamesHandler
lateinit var tinyIO: IO
var frontend_ready = false
val tinyNode = Node(this)
@@ -97,7 +99,10 @@ class MainActivity : Activity() {
null
) // disable acceleration, needed for older WebViews
}
- wai = WebAppInterface(this, webView)
+ gamesHandler = GamesHandler(idStore.identity)
+ webView.addJavascriptInterface(gamesHandler, "GameHandler")
+
+ wai = WebAppInterface(this, webView, gamesHandler)
// upgrades repo filesystem if necessary
tinyRepo.upgrade_repo()
tinyIO = IO(this, wai)
@@ -133,6 +138,8 @@ class MainActivity : Activity() {
*/
// val webStorage = WebStorage.getInstance()
webView.addJavascriptInterface(wai, "Android")
+ webView.addJavascriptInterface(gamesHandler, "GamesHandler")
+
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/WebAppInterface.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/WebAppInterface.kt
index b066535..7cb34f9 100644
--- a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/WebAppInterface.kt
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/WebAppInterface.kt
@@ -13,12 +13,19 @@ import android.webkit.WebView
import android.widget.Toast
import androidx.core.content.ContextCompat.checkSelfPermission
import com.google.zxing.integration.android.IntentIntegrator
+import org.json.JSONArray
import org.json.JSONObject
import nz.scuttlebutt.tremolavossbol.utils.Bipf
import nz.scuttlebutt.tremolavossbol.utils.Bipf.Companion.BIPF_BYTES
import nz.scuttlebutt.tremolavossbol.utils.Bipf.Companion.BIPF_LIST
+
+import nz.scuttlebutt.tremolavossbol.utils.Constants.Companion.TINYSSB_APP_C4_START
+import nz.scuttlebutt.tremolavossbol.utils.Constants.Companion.TINYSSB_APP_C4_DECLINE
+import nz.scuttlebutt.tremolavossbol.utils.Constants.Companion.TINYSSB_APP_C4_END
+import nz.scuttlebutt.tremolavossbol.utils.Constants.Companion.TINYSSB_APP_C4_INVITE
+
import nz.scuttlebutt.tremolavossbol.utils.Constants.Companion.TINYSSB_APP_IAM
import nz.scuttlebutt.tremolavossbol.utils.Constants.Companion.TINYSSB_APP_TEXTANDVOICE
import nz.scuttlebutt.tremolavossbol.utils.Constants.Companion.TINYSSB_APP_KANBAN
@@ -26,14 +33,19 @@ import nz.scuttlebutt.tremolavossbol.utils.Constants.Companion.TINYSSB_APP_SCHED
import nz.scuttlebutt.tremolavossbol.utils.HelperFunctions.Companion.deRef
import nz.scuttlebutt.tremolavossbol.utils.HelperFunctions.Companion.toBase64
import nz.scuttlebutt.tremolavossbol.utils.HelperFunctions.Companion.toHex
-import org.json.JSONArray
+import nz.scuttlebutt.tremolavossbol.games.battleships.BattleshipGame
+import nz.scuttlebutt.tremolavossbol.games.common.GamesHandler
+import nz.scuttlebutt.tremolavossbol.games.battleships.GameStates
+import nz.scuttlebutt.tremolavossbol.utils.Constants.Companion.TINYSSB_APP_GAMETEXT
+
// pt 3 in https://betterprogramming.pub/5-android-webview-secrets-you-probably-didnt-know-b23f8a8b5a0c
-class WebAppInterface(val act: MainActivity, val webView: WebView) {
+class WebAppInterface(val act: MainActivity, val webView: WebView, val gameHandler: GamesHandler) {
val frontend_frontier = act.getSharedPreferences("frontend_frontier", Context.MODE_PRIVATE)
+ var gamesHandler = gameHandler
@JavascriptInterface
fun onFrontendRequest(s: String) {
@@ -247,6 +259,113 @@ class WebAppInterface(val act: MainActivity, val webView: WebView) {
act.tinyNode.publish_public_content(body)
}
}
+ "games" -> { // Handle battleship communication
+ Log.d("GAM - WebApp", args.toString())
+ //gamesHandler.processGameRequest(s.substring(6))
+ // TODO here you can add restrictions, if a command is not allowed
+ if (args[1] == "BSH") {
+ when (args[2]) {
+ "INV" -> {
+ if (gamesHandler.getInviteCount("BSH") != 0) {
+ Log.d("BSH-Handler INV", "inviteCounter is not 0")
+ return
+ }
+ gameHandler.incInviteCount("BSH")
+ gamesHandler.addOwnGame(args[1], args[3], GameStates.INVITED)
+ val inst = gamesHandler.getInstanceFromFid(args[1], args[3])
+ (inst!!.game as BattleshipGame).setupGame(true)
+ val req = "${args[1]} INV ${args[3]} ${inst.startTime}"
+ public_post_game_request(
+ Base64.encodeToString(
+ req.toByteArray(),
+ Base64.NO_WRAP
+ )
+ )
+ return
+ }
+
+ "INVACC" -> {
+ val inst = gamesHandler.getInstanceFromFid("BSH", args[3])
+ var peerHash: String = ""
+ if (inst != null) {
+ (inst.game as BattleshipGame).setupGame(false)
+ peerHash = (inst.game as BattleshipGame).getShipPosition()
+ }
+
+ val invacc =
+ "BSH INVACC ${args[3]} ${gamesHandler.myId.toRef()} $peerHash" // Appending Peer's Shiphash
+ Log.d("GAM APP (INVACC)", invacc)
+ public_post_game_request(
+ Base64.encodeToString(
+ invacc.toByteArray(),
+ Base64.NO_WRAP
+ )
+ )
+ return
+ }
+
+ "SHOT" -> {
+ val inst = gamesHandler.getInstanceFromFids("BSH", args[3], args[4])
+ var isPeer: String = ""
+ if (gamesHandler.isIdEqualToMine(args[3])) { // I am owner
+ isPeer = "0"
+ } else if (gamesHandler.isIdEqualToMine(args[4])) {
+ isPeer = "1"
+ } else {
+ return
+ }
+ if (inst != null) {
+ if (!(inst.game as BattleshipGame).gameState!!.isMyTurn()) {
+ return
+ }
+ (inst.game as BattleshipGame).gameState!!.turn = false
+ } else {
+ return
+ }
+ val shot =
+ "${args[1]} ${args[2]} ${args[3]} ${args[4]} $isPeer ${args[5]}"
+ Log.d("GAM APP (SHOT)", shot)
+ public_post_game_request(
+ Base64.encodeToString(
+ shot.toByteArray(),
+ Base64.NO_WRAP
+ )
+ )
+ }
+
+ "DUELQUIT" -> {
+ val inst = gamesHandler.getInstanceFromFids("BSH", args[3], args[4])
+ if (inst == null || !inst.state.isActive()) {
+ return
+ }
+ inst.state = GameStates.STOPPED
+ (inst.game as BattleshipGame).gameState!!.turn = false
+ var isPeer: String = ""
+ if (gamesHandler.isIdEqualToMine(args[3])) { // I am owner
+ isPeer = "0"
+ } else if (gamesHandler.isIdEqualToMine(args[4])) {
+ isPeer = "1"
+ }
+ val quit = "${args[1]} ${args[2]} ${args[3]} ${args[4]} $isPeer"
+ Log.d("GAM APP (SHOT)", quit)
+ public_post_game_request(
+ Base64.encodeToString(
+ quit.toByteArray(),
+ Base64.NO_WRAP
+ )
+ )
+ }
+
+ else -> {
+ public_post_game_request(
+ Base64.encodeToString(
+ s.substring(6).toByteArray(), Base64.NO_WRAP
+ )
+ )
+ }
+ }
+ }
+ }
"settings:set" -> {
act.settings!!.set(args[1], args[2])
}
@@ -254,6 +373,20 @@ class WebAppInterface(val act: MainActivity, val webView: WebView) {
val settings = act.settings!!.getSettings()
act.wai.eval("b2f_get_settings('${settings}')")
}
+
+ "connect_four" -> {
+ connect_four(args[1], args[2], args[3], args[4]);
+ }
+ "connect_four_end" -> {
+ connect_four_end(args[1], args[2], args[3]);
+ }
+ "connect_four_invite" -> {
+ connect_four_invite(args[1], args[2]);
+ }
+ "connect_four_decline_invite" -> {
+ connect_four_decline_invite(args[1],args[2]);
+ }
+
else -> {
Log.d("onFrontendRequest", "unknown")
}
@@ -339,6 +472,19 @@ class WebAppInterface(val act: MainActivity, val webView: WebView) {
act.tinyNode.publish_public_content(body_encr)
}
+ // BATTLESHIP
+ fun public_post_game_request(text: String?) {
+ val lst = Bipf.mkList()
+ Bipf.list_append(lst, TINYSSB_APP_GAMETEXT)
+ Bipf.list_append(lst, if (text == null) Bipf.mkNone() else Bipf.mkString(text))
+ val tst = Bipf.mkInt((System.currentTimeMillis() / 1000).toInt())
+ Log.d("wai", "send time is ${tst.getInt()}")
+ Bipf.list_append(lst, tst)
+ val body = Bipf.encode(lst)
+ if (body != null)
+ act.tinyNode.publish_public_content(body)
+ }
+
fun scheduling(bid: String?, prev: List?, operation: String, args: List?) {
val lst = Bipf.mkList()
Bipf.list_append(lst, TINYSSB_APP_SCHEDULING)
@@ -420,6 +566,64 @@ class WebAppInterface(val act: MainActivity, val webView: WebView) {
}
+ fun connect_four(gameId: String, currentPlayer: String, members: String, stonePos: String) {
+ val lst = Bipf.mkList()
+ Bipf.list_append(lst, TINYSSB_APP_C4_START)
+ Bipf.list_append(lst, Bipf.mkString(gameId))
+ Bipf.list_append(lst, Bipf.mkString(currentPlayer))
+ Bipf.list_append(lst, Bipf.mkString(members))
+ Bipf.list_append(lst, Bipf.mkString(stonePos))
+
+ val body = Bipf.encode(lst)
+
+ if (body != null) {
+ Log.d("connect_four", "published bytes: " + Bipf.decode(body))
+ act.tinyNode.publish_public_content(body)
+ }
+ }
+
+ fun connect_four_invite(inviter: String, invitee: String) {
+ val lst = Bipf.mkList()
+ Bipf.list_append(lst, TINYSSB_APP_C4_INVITE)
+ Bipf.list_append(lst, Bipf.mkString(inviter))
+ Bipf.list_append(lst, Bipf.mkString(invitee))
+
+ val body = Bipf.encode(lst)
+
+ if (body != null) {
+ Log.d("connect_four_invite", "published bytes: " + Bipf.decode(body))
+ act.tinyNode.publish_public_content(body)
+ }
+ }
+
+ fun connect_four_decline_invite(inviter: String, invitee: String) {
+ val lst = Bipf.mkList()
+ Bipf.list_append(lst, TINYSSB_APP_C4_DECLINE)
+ Bipf.list_append(lst, Bipf.mkString(inviter))
+ Bipf.list_append(lst, Bipf.mkString(invitee))
+
+ val body = Bipf.encode(lst)
+
+ if (body != null) {
+ Log.d("connect_four_decline_invite", "published bytes: " + Bipf.decode(body))
+ act.tinyNode.publish_public_content(body)
+ }
+ }
+
+ fun connect_four_end(gameId: String, loser: String, stonePos: String) {
+ val lst = Bipf.mkList()
+ Bipf.list_append(lst, TINYSSB_APP_C4_END)
+ Bipf.list_append(lst, Bipf.mkString(gameId))
+ Bipf.list_append(lst, Bipf.mkString(loser))
+ Bipf.list_append(lst, Bipf. mkString(stonePos))
+
+ val body = Bipf.encode(lst)
+ if (body != null) {
+ Log.d("connect_four", "published bytes: " + Bipf.decode(body))
+ act.tinyNode.publish_public_content(body)
+ }
+ }
+
fun return_voice(voice: ByteArray) {
var cmd = "b2f_new_voice('" + voice.toBase64() + "');"
Log.d("CMD", cmd)
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/BattleshipGame.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/BattleshipGame.kt
new file mode 100644
index 0000000..a506547
--- /dev/null
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/BattleshipGame.kt
@@ -0,0 +1,218 @@
+package nz.scuttlebutt.tremolavossbol.games.battleships
+
+import nz.scuttlebutt.tremolavossbol.games.common.GameInterface
+import kotlin.random.Random
+
+/**
+ * The main class of the game. Everything can be managed through this class (hopefully). It is a
+ * simple battleships implementation working asymmetrically without the enemy field.
+ * The coordinate field looks as follows:
+ *
+ * X-Axis 0 1 2 3 4 5 6 7 8 9
+ * Y-Axis
+ * 0 ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
+ * 1 ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
+ * 2 ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
+ * 3 ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
+ * 4 ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
+ * 5 ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
+ * 6 ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
+ * 7 ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
+ * 8 ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
+ * 9 ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
+ */
+class BattleshipGame : GameInterface {
+ private val DEFAULT_CONFIG = arrayOf(2, 3, 3, 4, 5)
+ var gameState: GameState? = null
+ override var state: GameStates = GameStates.INVITED
+ override var isRunning: Boolean = false
+
+ /**
+ * This method defines the ships that will be used in the game. It takes an array of integers,
+ * where each integer represents one ship of that length. By default, the classic rules will be used:
+ * 2, 3, 3, 4, 5
+ *
+ * Will do nothing if the game has already started
+ *
+ * @param turn True if it is your turn, false if not
+ * @param ships An array of integers containing the ship lengths, has a default configuration
+ * of 2 3 3 4 5
+ *
+ */
+ fun setupGame(
+ turn: Boolean,
+ ships: Array = DEFAULT_CONFIG
+ ) {
+ if (isRunning) return
+ gameState =
+ GameState(
+ turn,
+ ships
+ )
+ isRunning = true
+ // Randomly placing the ships. Makes is more convenient.
+ for (i in ships.indices) {
+ var direction = Direction.values().random()
+ var x = Random.nextInt(0,10)
+ var y = Random.nextInt(0,10)
+ while (!placeShip(i,x,y, direction)) {
+ direction = Direction.values().random()
+ x = Random.nextInt(0,10)
+ y = Random.nextInt(0,10)
+ }
+ }
+ }
+
+ /**
+ * Checks if the enemy has won the game. Returns false if the game has not started yet.
+ *
+ * @return True if the enemy has won
+ */
+ fun enemyHasWon(): Boolean {
+ if (!isRunning) return false
+ return gameState!!.enemyHasWon()
+ }
+
+ /**
+ * Ends the currently running game deleting all information about it
+ */
+ fun endGame() {
+ isRunning = false
+ gameState = null
+ }
+
+ /**
+ * Tries to place a ship on your own battlefield
+ *
+ * @return true if the ship was placed, false if the placement was invalid
+ */
+ fun placeShip(
+ index: Int,
+ x: Int,
+ y: Int,
+ direction: Direction
+ ): Boolean {
+ if (!isRunning) return false
+ return gameState!!.placeShip(
+ index,
+ x,
+ y,
+ direction
+ )
+ }
+
+ /**
+ * Shoots a shot to the enemy while checking its validity. Duplicates are invalid
+ * @param x The x coordinate of the shot
+ * @param y The y coordinate of the shot
+ *
+ * @return true if the shot is valid, false if the shot is not
+ */
+ fun shoot(
+ x: Int,
+ y: Int
+ ): Boolean {
+ if (!isRunning) return false
+ return gameState!!.shoot(
+ x,
+ y
+ )
+ }
+
+ /**
+ * Receive a shot from the enemy.
+ *
+ * @param x The x coordinate of the shot
+ * @param y The y coordinate of the shot
+ *
+ * @return A ShotOutcome describing what happened (HIT/MISS/SUNKEN)
+ *
+ * @throws IllegalStateException If the game has not yet started
+ */
+ fun receiveShot(
+ x: Int,
+ y: Int
+ ): ShotOutcome {
+ if (!isRunning) throw IllegalStateException("Game has not yet started")
+ try {
+ return gameState!!.receiveShot(
+ x,
+ y
+ )
+ } catch (e: Exception) {
+ throw IllegalStateException("It is your turn")
+ }
+ }
+
+ /**
+ * Receive the outcome of your shot from the enemy. If there are multiple outcomes for the
+ * same coordinates, it will save the latest
+ *
+ * @param x The x coordinate of the shot
+ * @param y The y coordinate of the shot
+ * @param shotOutcome The outcome of the shot
+ */
+ fun shotOutcome(x: Int, y: Int, shotOutcome: ShotOutcome) {
+ if (!isRunning) return
+ gameState!!.shotOutcome(x, y, shotOutcome)
+ }
+
+ /**
+ * Serializes the game state to send it to the frontend.
+ * Format:
+ * ShipPosition^ShotsFired^ShotsReceived^Turn
+ * Example:
+ * 28UP~73RIGHT^43MISS~15HIT^85SUNKEN~24MISS^true
+ *
+ * @return Empty string if the game is not running, the serialized state otherwise
+ */
+ fun serialize(): String {
+ return gameState!!.serialize()
+ }
+
+ /**
+ * Returns position of ships in following format:
+ * 28UP~83RIGHT~21UP~73RIGHT
+ *
+ * @return Empty string if the game is not running
+ */
+ fun getShipPosition(): String {
+ if (!isRunning) return ""
+ return gameState!!.getShipPosition()
+ }
+
+ /**
+ * Returns an array of all ship positions of the ship which has a tile on position x, y.
+ * Returns an empty array if there is no ship with a tile on that position.
+ */
+ fun getShipAtPosition(x: Int, y: Int): Array {
+ return gameState!!.getShipAtPosition(x, y)
+ }
+
+ /**
+ * Returns the shots you fired along with the outcomes in a list. This could be useful for
+ * anti-cheat detection or sending spectators past movements.
+ */
+ fun shotsFiredWithOutcome(): String {
+ return gameState!!.shotsFiredWithOutcomeToString()
+ }
+
+ override fun toString(): String {
+ return "BSH"
+ }
+
+ /**
+ * This method registers ACK'd moves of the Owner. This is later useful to spectate a game.
+ */
+ fun registerSpectatorOwner(x: Int, y: Int, outcome: ShotOutcome) {
+ gameState!!.shotOutcome(x,y,outcome)
+ }
+
+ /**
+ * This method registers ACK'd moves of the Peer. This is later useful to spectate a game.
+ */
+ fun registerSpectatorPeer(x: Int, y: Int, outcome: ShotOutcome) {
+ //gameState!!.shotReceived.add(Pair(Position2D(x,y), outcome))
+ gameState!!.receiveShot(x,y)
+ }
+}
\ No newline at end of file
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/BattleshipHandler.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/BattleshipHandler.kt
new file mode 100644
index 0000000..06d988f
--- /dev/null
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/BattleshipHandler.kt
@@ -0,0 +1,582 @@
+package nz.scuttlebutt.tremolavossbol.games.battleships
+
+import android.util.Log
+import nz.scuttlebutt.tremolavossbol.crypto.SodiumAPI.Companion.sha256
+import nz.scuttlebutt.tremolavossbol.games.common.GameInstance
+import nz.scuttlebutt.tremolavossbol.games.common.GameInterface
+import nz.scuttlebutt.tremolavossbol.games.common.GamesHandler
+
+
+/**
+ * Represents the set of all Battleship games. Games are managed in a list of GameInstances.
+ * Instances can be addressed with the fid and the getInstanceFromFid function.
+ */
+class BattleshipHandler(val gameHandler: GamesHandler) {
+ private val gamesHandler: GamesHandler = gameHandler
+ private val instances: MutableList = mutableListOf()
+ var inviteCounter: Int = 0
+
+ /**
+ * This is the main entry function, which parses the given String s to achieve the
+ * desired result. (Frontend-Requests)
+ */
+ fun handleRequest(s: String, game: GameInstance?): String {
+ Log.d("BSH Handler", s)
+ val args = s.split(" ")
+ when (args[0]) { // 0 = games, 1 = BSH
+ "INV" -> { // ATBC
+ val id = args[1]
+ if (gamesHandler.isIdEqualToMine(id)) { // i am the owner
+ return ""
+ }
+ var inst = gamesHandler.getInstanceFromFid("BSH", id)
+ if (inst == null) {
+ gamesHandler.addOwnGame("BSH", id, GameStates.INVITED)
+ inst = gamesHandler.getInstanceFromFid("BSH", id)
+ (inst!!.game as BattleshipGame).setupGame(false)
+ }
+ inst.startTime = args[2].toLong()
+ Log.d("BSH-Handler", "Added new gameInstance $id")
+ return ""
+ }
+ "INVACC" -> { // Peer Accepted Invite: OID PID P.Hash
+ Log.d("BSH-Handler INVACC", args.toString())
+ val ownerID = args[1]
+ val peerID = args[2]
+ val inst = gamesHandler.getInstanceFromFid("BSH", ownerID) // TODO peerID noch nicht hinterlegt wenn INV
+ // Only Owner of the game is allowed to answer
+ if (inst != null && gamesHandler.isIdEqualToMine(ownerID)) {
+ Log.d("BSH-Handler INVACC", "Found empty Game.")
+ // Todo add O.Ship# to String
+ inst.state = GameStates.RUNNING
+ inst.participantFid = peerID
+ (inst.game as BattleshipGame).gameState!!.enemyHash = args[3]
+ val ownerHash = (inst.game as BattleshipGame).gameState!!.getShipPosition()
+ return "games BSH DUELACC $ownerID $peerID $ownerHash!CERBERUS!$ownerID $peerID"
+ } else if (gamesHandler.isIdEqualToMine(ownerID)) {
+ Log.d("BSH-Handler INVACC", "Im Owner and inst is null.")
+ return "games BSH DUELDEC $ownerID $peerID"
+ }
+ Log.d("BSH-Handler INVACC", "Im Peer or nobody.")
+ return ""
+ }
+ "DUELACC" -> { // OID PID O.Hash
+ // Message for Peer
+ val ownerID = args[1]
+ val peerID = args[2]
+ if (gamesHandler.isIdEqualToMine(peerID)) {
+ val inst = gamesHandler.getInstanceFromFid("BSH", ownerID)
+ if (inst != null) {
+ (inst.game as BattleshipGame).setupGame(false)
+ inst.participantFid = peerID
+ inst.state = GameStates.RUNNING
+ (inst.game as BattleshipGame).gameState!!.enemyHash = args[3]
+ }
+ return "!CERBERUS!$ownerID $peerID"
+ } else if (!gamesHandler.isIdEqualToMine(ownerID)) {
+ val inst = gamesHandler.getInstanceFromFid("BSH", ownerID)
+ if (inst != null) {
+ inst.state = GameStates.SPEC
+ inst.participantFid = peerID
+ (inst.game as BattleshipGame).gameState!!.overwriteSpectatorOwnerShip(args[3])
+ }
+ }
+ return "!CERBERUS!$ownerID $peerID"
+ }
+ "DUELDEC" -> {
+ // Message for Peer
+ val ownerID = args[1]
+ val peerID = args[2]
+ if (gamesHandler.isIdEqualToMine(peerID)) {
+ val inst = gamesHandler.getInstanceFromFids("BSH", ownerID, peerID) // if already ingame dismiss
+ if (inst != null) {
+ if (gamesHandler.isIdEqualToMine(ownerID) || gamesHandler.isIdEqualToMine(peerID)) {
+ return ""
+ }
+ return ""
+ } else {
+ val inst2 = gamesHandler.getInstanceFromFid("BSH", ownerID)
+ if (inst2 != null) {
+ if (gamesHandler.isIdEqualToMine(ownerID)) {
+ return ""
+ } else if (gamesHandler.isIdEqualToMine(peerID)) { // No Peer's your game
+ inst2.state = GameStates.SPEC
+ }
+ }
+ }
+ }
+ return ""
+ }
+ "SHOT" -> { // OID PID isPeer Pos
+ // Turn auf false
+ val ownerID = args[1]
+ val peerID = args[2]
+ val inst = gamesHandler.getInstanceFromFids("BSH", ownerID, peerID)
+ val isPeer = args[3]
+ val x = args[4][0].toString().toInt()
+ val y = args[4][1].toString().toInt()
+
+ if (gamesHandler.isIdEqualToMine(ownerID) && isPeer == "1") {
+ if (inst != null) {
+ val outcome: String
+ try {
+ outcome = (inst.game as BattleshipGame).receiveShot(x,y).toString() // pos extracting
+ if (outcome == "M") {
+ (inst.game as BattleshipGame).gameState!!.turn = true
+ }
+ } catch (e: IllegalStateException) {
+ return "games BSH SHOTDEC $ownerID $peerID $isPeer"
+ }
+ if ((inst.game as BattleshipGame).gameState!!.enemyHasWon()) {
+ inst.state = GameStates.LOST
+ gamesHandler.decInviteCount("BSH")
+ return "games BSH DUELWON $ownerID $peerID 0 ${(inst.game as BattleshipGame).gameState!!.shotsReceivedWithOutcomeToString()} ${(inst.game as BattleshipGame).gameState!!.shotsFiredWithOutcomeToString()}!CERBERUS!$ownerID $peerID"
+ }
+ // TODO update received shots
+ return "games BSH SHOTACC $ownerID $peerID $isPeer $x$y$outcome ${(inst.game as BattleshipGame).gameState!!.shotsReceivedWithOutcomeToString()} ${(inst.game as BattleshipGame).gameState!!.shotsFiredWithOutcomeToString()}"
+ }
+ } else if (gamesHandler.isIdEqualToMine(peerID) && isPeer == "0") { // i am peer, owner sent
+ Log.d("BSH Handler: ", "Peer Received Shot")
+ if (inst != null && !(inst.game as BattleshipGame).gameState!!.isMyTurn()) {
+ val outcome: String
+ try {
+ outcome = (inst.game as BattleshipGame).receiveShot(x,y).toString() // pos extracting
+ if (outcome == "M") {
+ (inst.game as BattleshipGame).gameState!!.turn = true
+ }
+ } catch (e: IllegalStateException) {
+ return "games BSH SHOTDEC $ownerID $peerID $isPeer"
+ }
+ if ((inst.game as BattleshipGame).gameState!!.enemyHasWon()) {
+ inst.state = GameStates.LOST
+ gamesHandler.decInviteCount("BSH")
+ return "games BSH DUELWON $ownerID $peerID 1 $x$y$outcome ${(inst.game as BattleshipGame).gameState!!.shotsReceivedWithOutcomeToString()} ${(inst.game as BattleshipGame).gameState!!.shotsFiredWithOutcomeToString()}!CERBERUS!$ownerID $peerID"
+ }
+ // TODO update received shots
+ return "games BSH SHOTACC $ownerID $peerID $isPeer $x$y$outcome ${(inst.game as BattleshipGame).gameState!!.shotsReceivedWithOutcomeToString()} ${(inst.game as BattleshipGame).gameState!!.shotsFiredWithOutcomeToString()}"
+ }
+ }
+ return ""
+ }
+ "SHOTACC" -> { // OID PID isPeer Pos Outcome (ATBC)
+ val ownerID = args[1]
+ val peerID = args[2]
+ var inst = gamesHandler.getInstanceFromFids("BSH", ownerID, peerID)
+ val x = args[4][0].toString().toInt()
+ val y = args[4][1].toString().toInt()
+ val outcome : ShotOutcome? = ShotOutcome.getFromString(args[4][2].toString())
+ if (inst != null) {
+ if (gamesHandler.isIdEqualToMine(ownerID) && args[3] == "0" && outcome != null) { // Owner's shot
+ (inst.game as BattleshipGame).shotOutcome(x,y, outcome)
+ if (outcome == ShotOutcome.HIT || outcome == ShotOutcome.SUNKEN) {
+ (inst.game as BattleshipGame).gameState!!.turn = true
+ } else {
+ (inst.game as BattleshipGame).gameState!!.turn = false
+ }
+ } else if (gamesHandler.isIdEqualToMine(peerID) && args[3] == "1" && outcome != null) {// Peer's shot
+ (inst.game as BattleshipGame).shotOutcome(x,y, outcome)
+ if (outcome == ShotOutcome.HIT || outcome == ShotOutcome.SUNKEN) {
+ (inst.game as BattleshipGame).gameState!!.turn = true
+ } else {
+ (inst.game as BattleshipGame).gameState!!.turn = false
+ }
+ } else if (!gamesHandler.isIdEqualToMine(peerID) && !gamesHandler.isIdEqualToMine(ownerID)) { // Im neither owner nor peer
+ // TODO participantShots, ownerShots for spectator
+ // TODO update delivered shots
+ if (args[3] == "1" && outcome != null) {
+ (inst.game as BattleshipGame).registerSpectatorPeer(x,y, outcome)
+ } else if (args[3] == "0" && outcome != null) {
+ (inst.game as BattleshipGame).registerSpectatorOwner(x,y, outcome)
+ }
+ }
+ } else {
+ gamesHandler.addOwnGame("BSH", ownerID, GameStates.SPEC)
+ inst = gamesHandler.getInstanceFromFid("BSH", ownerID)
+ inst!!.setParticipant(peerID)
+ }
+ return ""
+ }
+ "SHOTDEC" -> {
+ val ownerID = args[1]
+ val peerID = args[2]
+ var inst = gamesHandler.getInstanceFromFids("BSH", ownerID, peerID)
+ if (gamesHandler.isIdEqualToMine(peerID) && args[3] == "1") { // It was Peer's shot
+ if (inst != null) {
+ (inst.game as BattleshipGame).gameState!!.turn = true
+ }
+ } else if (gamesHandler.isIdEqualToMine(ownerID) && args[3] == "0") { // It was Owner's shot
+ if (inst != null) {
+ (inst.game as BattleshipGame).gameState!!.turn = true
+ }
+ }
+ return ""
+ }
+ "DUELWON" -> {
+ val ownerID = args[1]
+ val peerID = args[2]
+ val x = args[4][0].toString().toInt()
+ val y = args[4][1].toString().toInt()
+ val outcome : ShotOutcome? = ShotOutcome.getFromString(args[4][2].toString())
+ val isPeer = args[3]
+ val inst = gamesHandler.getInstanceFromFids("BSH", ownerID, peerID)
+ if (gamesHandler.isIdEqualToMine(ownerID) && isPeer == "1" && outcome != null) { // Owner Won
+ if (inst != null) {
+ (inst.game as BattleshipGame).shotOutcome(x,y,outcome)
+ inst.state = GameStates.WON
+ }
+ // TODO null setzen instance? Was passiert mit fertigen Games?
+ gamesHandler.decInviteCount("BSH")
+ return "!CERBERUS!$ownerID $peerID"
+ } else if (gamesHandler.isIdEqualToMine(peerID) && isPeer == "0" && outcome != null) { // Peer has won
+ if (inst != null) {
+ (inst.game as BattleshipGame).shotOutcome(x,y,outcome)
+ inst.state = GameStates.WON
+ }
+ gamesHandler.decInviteCount("BSH")
+ return "!CERBERUS!$ownerID $peerID"
+ } else if (!((gamesHandler.isIdEqualToMine(peerID) && isPeer == "1") || (gamesHandler.isIdEqualToMine(ownerID) && isPeer == "0"))) {
+ if (inst != null && outcome != null) {
+ if (isPeer == "0") {
+ (inst.game as BattleshipGame).registerSpectatorOwner(x,y,outcome)
+ } else {
+ (inst.game as BattleshipGame).registerSpectatorPeer(x,y,outcome)
+ }
+ inst.state = GameStates.STOPPED
+ }
+ return "!CERBERUS!$ownerID $peerID"
+ }
+ return ""
+ }
+ "DUELQUIT" -> {
+ val ownerID = args[1]
+ val peerID = args[2]
+ val isPeer = args[3]
+ val inst = gamesHandler.getInstanceFromFids("BSH", ownerID, peerID)
+ if (gamesHandler.isIdEqualToMine(ownerID) && isPeer == "1") { // Peer Quit
+ if (inst != null) {
+ inst.state = GameStates.STOPPED
+ (inst.game as BattleshipGame).gameState!!.turn = false
+ }
+ gamesHandler.decInviteCount("BSH")
+ } else if (gamesHandler.isIdEqualToMine(peerID) && isPeer == "0") { // Owner Quit
+ if (inst != null) {
+ inst.state = GameStates.STOPPED
+ (inst.game as BattleshipGame).gameState!!.turn = false
+ }
+ } else {
+ if (inst != null) {
+ inst.state = GameStates.STOPPED
+ }
+ }
+ return ""
+ }
+ else -> {
+ Log.e("BSH Handler", s)
+ return ""
+ }
+ }
+ }
+
+ /**
+ * Returns the GameInstance with the specified fid. GameInstance is null if no game found.
+ */
+ fun getInstanceFromFid(fid: String): GameInstance? {
+ var instance: GameInstance? = null
+ for (game in instances) {
+ if (fid == game.participantFid) {
+ instance = game
+ }
+ }
+ return instance
+ }
+
+ // ---- Methods of the previous group. Sadly we did not have time to make our code more readable.
+ // ---- For better readability we suggest taking a look at the above protocol and create methods in each
+ // ---- object we use to have a more structured program.
+
+
+ /**
+ * Adds a shot to the Battleship game.
+ *
+ * @param fid The id of the game
+ * @param position the position of the shot, a double digit number where x is the first
+ * digit and y the second
+ *
+ * @return false if the shot is not valid in any way, true if it registered
+ */
+ fun shoot(fid: String, position: String): Boolean {
+ val instance = getInstanceFromFid(fid)
+ if (instance == null) {
+ Log.d("Battleship Shoot", "Game not found")
+ return false
+ }
+ if (position.length != 2) {
+ Log.d("Battleship Shoot", "Wrong number of coordinates in Shot Position")
+ return false
+ }
+ val bshGame : GameInterface? = instance.game
+ if (bshGame !is BattleshipGame) {
+ Log.d("Battlehip Shoot", "Instance is not BSH-Game")
+ return false
+ }
+ return bshGame.shoot(position[0].digitToInt(), position[1].digitToInt())
+ }
+
+ /**
+ * Adds a receiveShot to the Battleship game.
+ *
+ * @param fid The id of the game
+ * @param position the position of the shot, a double digit number where x is the first
+ * digit and y the second
+ *
+ * @return false if the shot is not valid in any way, true if it registered
+ */
+ fun receiveShot(fid: String, position: String): ShotOutcome {
+ val instance = getInstanceFromFid(fid)
+ if (instance == null) {
+ Log.d("Battleship Receive Shot", "Game not found")
+ return ShotOutcome.MISS
+ }
+ if (position.length != 2) {
+ Log.d("Battleship Receive Shot", "Wrong number of coordinates in Shot Position")
+ return ShotOutcome.MISS
+ }
+ val bshGame : GameInterface? = instance.game
+ if (bshGame !is BattleshipGame) {
+ Log.d("Battlehip Receive Shot", "Instance is not BSH-Game")
+ return ShotOutcome.MISS
+ }
+ return bshGame.receiveShot(position[0].digitToInt(), position[1].digitToInt())
+ }
+
+ /**
+ * Checks if enemy has sunken all ships.
+ */
+ fun checkEnemyHasWon(fid: String): Boolean {
+ val instance = getInstanceFromFid(fid)
+ if (instance == null) {
+ Log.d("Battleship Check Enemy Win", "Game not found")
+ return false
+ }
+ val bshGame : GameInterface? = instance.game
+ if (bshGame !is BattleshipGame) {
+ Log.d("Battlehip checkEnemyWon", "Instance is not BSH-Game")
+ return false
+ }
+ return bshGame.enemyHasWon()
+ }
+
+ fun stopGame(fid: String) {
+ val instance = getInstanceFromFid(fid)
+ if (instance == null) {
+ Log.d("Battleship Stop Game", "Game not found")
+ return
+ }
+ val bshGame : GameInterface? = instance.game
+ if (bshGame !is BattleshipGame) {
+ Log.d("Battlehip Stop Game", "Instance is not BSH-Game")
+ return
+ }
+ bshGame.endGame()
+ }
+
+ /**
+ * Saves shot outcome in game logic.
+ */
+ fun saveShotOutcome(fid: String, msg: String) {
+ val instance = getInstanceFromFid(fid)
+ if (instance == null) {
+ Log.d("Battleship Save Shot Outcome", "Game not found")
+ return
+ }
+ val bshGame : GameInterface? = instance.game
+ if (bshGame !is BattleshipGame) {
+ Log.d("Battlehip saveShotOutcome", "Instance is not BSH-Game")
+ return
+ }
+ val xValue = msg[0].digitToInt()
+ val yValue = msg[1].digitToInt()
+ when (msg.substring(2)) {
+ "MISS" -> {
+ bshGame.shotOutcome(xValue, yValue, ShotOutcome.MISS)
+ }
+ "HIT" -> {
+ bshGame.shotOutcome(xValue, yValue, ShotOutcome.HIT)
+ }
+ "SUNKEN" -> {
+ bshGame.shotOutcome(xValue, yValue, ShotOutcome.SUNKEN)
+ }
+ }
+ }
+
+ /**
+ * Returns a String containing all information about a game. String format:
+ * State^ShipPosition^ShotsFired^ShotsReceived^Turn
+ * Example: RUNNING^28UP~82DOWN~11RIGHT~99LEFT^23MISS~45MISS~56HIT~57SUNKEN^73MISS~48MISS~11HIT~12SUNKEN^True
+ */
+ fun serialize(fid: String): String {
+ val instance = getInstanceFromFid(fid)
+ if (instance == null) {
+ Log.d("Battleship Get Game State", "Game not found")
+ return "STOPPED"
+ }
+ val bshGame : GameInterface? = instance.game
+ if (bshGame !is BattleshipGame) {
+ Log.d("Battlehip serialize", "Instance is not BSH-Game")
+ return "STOPPED"
+ }
+ return instance.state.toString() + "^" + bshGame.serialize()
+ }
+
+ /**
+ * Returns a String containing Ship positions in the following format:
+ * 28UP~82DOWN~11RIGHT~99LEFT
+ * It is used for the hash calculation and cheat detection.
+ */
+ fun getShipPosition(fid: String): String {
+ val instance = getInstanceFromFid(fid)
+ if (instance == null) {
+ Log.d("Battleship Get Ship Position", "Game not found")
+ return ""
+ }
+ val bshGame : GameInterface? = instance.game
+ if (bshGame !is BattleshipGame) {
+ Log.d("Battlehip getShipPosition", "Instance is not BSH-Game")
+ return ""
+ }
+ return bshGame.getShipPosition()
+ }
+
+ /**
+ * Returns all positions of the ship that is at the given coordinates.
+ * Returns an empty array if there is no ship at the given coordinates
+ */
+ fun getShipAtPosition(fid: String, position: String): Array {
+ val instance = getInstanceFromFid(fid)
+ if (instance == null) {
+ Log.d("Battleship Get Ship At Position", "Game not found")
+ return arrayOf()
+ }
+ val bshGame : GameInterface? = instance.game
+ if (bshGame !is BattleshipGame) {
+ Log.d("Battlehip getShipAtPosition", "Instance is not BSH-Game")
+ return arrayOf()
+ }
+ return bshGame.getShipAtPosition(position[0].digitToInt(), position[1].digitToInt())
+ }
+
+ /**
+ * Compares the saved hash with the ship positions as well as the ship positions with the game history.
+ * Used for possible cheat detection.
+ *
+ * @return Returns True if the hashes were equal and the ship positions match the game history.
+ */
+ fun checkPositions(fid: String, shipPos: String): Boolean {
+ val instance = getInstanceFromFid(fid)
+ if (instance == null) {
+ Log.d("Battleship Check Hash", "Game not found")
+ return false
+ }
+ val bshGame : GameInterface? = instance.game
+ if (bshGame !is BattleshipGame) {
+ Log.d("Battlehip checkPositions", "Instance is not BSH-Game")
+ return false
+ }
+ return (String(shipPos.encodeToByteArray().sha256()) == bshGame.gameState!!.enemyHash
+ && checkIfHitsLineUpWithEnemyPositions(
+ bshGame.gameState!!.shotsFiredWithOutcome,
+ shipPos
+ ))
+ }
+
+ /**
+ * Sets the state of the game specified by the fid.
+ */
+ fun setState(fid: String, state: GameStates) {
+ val instance = getInstanceFromFid(fid)
+ if (instance == null) {
+ Log.d("Battleship Set State", "Game not found")
+ return
+ }
+ instance.state = state
+ }
+
+ /**
+ * Given the shots and the enemy ship positions, this function checks that all shots were
+ * reported correctly from the enemy.
+ *
+ * @param shots Pairs of positions and shot outcomes of shots this player shot
+ * @param enemyShips String describing the enemy ship positions, example 34DOWN~54UP~12RIGHT~53LEFT~97UP
+ *
+ * @return true if all shots are legit, false if there is a conflict
+ */
+ private fun checkIfHitsLineUpWithEnemyPositions(
+ shots: MutableList>,
+ enemyShips: String
+ ): Boolean {
+ val shipLengths = arrayOf(2, 3, 3, 4, 5)
+ val enemyPositions = MutableList(0) { Position2D(0, 0) }
+ val enemyShipsSplit = enemyShips.split("~")
+ enemyShipsSplit.forEachIndexed { i, ship ->
+ val x = ship[0].digitToInt()
+ val y = ship[1].digitToInt()
+ val direction = when (ship.substring(2)) {
+ Direction.UP.string -> Direction.UP
+ Direction.DOWN.string -> Direction.DOWN
+ Direction.LEFT.string -> Direction.LEFT
+ Direction.RIGHT.string -> Direction.RIGHT
+ else -> {
+ return false
+ }
+ }
+ for (j in 0 until shipLengths[i]) {
+ enemyPositions.add(getPosition(x, y, j, direction))
+ }
+ }
+ enemyPositions.forEach {
+ Log.d(
+ "Battleship Calculated enemy position",
+ "(x, y): (${it.getXPosition()}, ${it.getYPosition()})"
+ )
+ }
+ shots.forEach {
+ if ((it.second == ShotOutcome.SUNKEN || it.second == ShotOutcome.HIT) && !enemyPositions.contains(
+ it.first
+ )
+ ) {
+ Log.d(
+ "Battleship Incorrect Hit or Sunken",
+ "Detected cheat position: (${it.first.getXPosition()}, ${it.first.getYPosition()})"
+ )
+ return false
+ }
+ if (it.second == ShotOutcome.MISS && enemyPositions.contains(it.first)) {
+ Log.d(
+ "Battleship Incorrect Miss",
+ "Detected cheat position: (${it.first.getXPosition()}, ${it.first.getYPosition()})"
+ )
+ return false
+ }
+ }
+ return true
+ }
+
+ /**
+ * Helper function, just a wrapped switch case to calculate the i-th position
+ *
+ * @param x The x coordinate of the anchor point
+ * @param y The y coordinate of the anchor point
+ * @param i The i-th position gets calculated
+ * @param direction The direction the ship faces
+ *
+ * @return The i-th position of that ship
+ */
+ private fun getPosition(x: Int, y: Int, i: Int, direction: Direction): Position2D {
+ return when (direction) {
+ Direction.UP -> Position2D(x, y - i)
+ Direction.DOWN -> Position2D(x, y + i)
+ Direction.RIGHT -> Position2D(x + i, y)
+ Direction.LEFT -> Position2D(x - i, y)
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/Direction.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/Direction.kt
new file mode 100644
index 0000000..b025b0f
--- /dev/null
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/Direction.kt
@@ -0,0 +1,49 @@
+package nz.scuttlebutt.tremolavossbol.games.battleships
+
+/**
+ * Simple enum to define directions on the playing field
+ */
+enum class Direction(val string: String) {
+ UP("UP"),
+ DOWN("DOWN"),
+ RIGHT("RIGHT"),
+ LEFT("LEFT");
+
+ override fun toString(): String {
+ when (this.string) {
+ "UP" -> {
+ return "U"
+ }
+ "DOWN" -> {
+ return "D"
+ }
+ "LEFT" -> {
+ return "L"
+ }
+ "RIGHT" -> {
+ return "R"
+ }
+ }
+ return ""
+ }
+
+ companion object {
+ fun fromString(s: String): Direction?{
+ when (s) {
+ "U" -> {
+ return UP
+ }
+ "D" -> {
+ return DOWN
+ }
+ "L" -> {
+ return LEFT
+ }
+ "R" -> {
+ return RIGHT
+ }
+ }
+ return null
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/GameState.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/GameState.kt
new file mode 100644
index 0000000..91200d7
--- /dev/null
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/GameState.kt
@@ -0,0 +1,315 @@
+package nz.scuttlebutt.tremolavossbol.games.battleships
+import android.util.Log
+
+/**
+ * This class represents the current state of the game.
+ *
+ * @param shipSizes An array of all the boat sizes
+ */
+class GameState(
+ var turn: Boolean,
+ shipSizes: Array
+) {
+ private val sizes: Array = shipSizes
+ private val ships =
+ Array(
+ shipSizes.size
+ ) { i ->
+ Ship(
+ shipSizes[i]
+ )
+ }
+ val shotsFired =
+ mutableListOf()
+ val shotReceived =
+ mutableListOf>()
+ val shotsFiredWithOutcome =
+ mutableListOf>()
+ val moveValidator =
+ MoveValidator()
+ var enemyHash: String? = null
+
+
+ /**
+ * Sets the boat on your own battlefield
+ *
+ * @param index: The index of the boat getting set
+ * @param x: X coordinate of the anchorpoint
+ * @param y: Y coordinate of the anchorpoint
+ * @param direction: The direction the boat is facing starting from the anchor point
+ */
+ fun placeShip(
+ index: Int,
+ x: Int,
+ y: Int,
+ direction: Direction
+ ): Boolean {
+ ships[index].setPosition(
+ x,
+ y,
+ direction
+ )
+ if (moveValidator.isValidPlacement(
+ ships,
+ index
+ )
+ ) return true
+ ships[index].setPosition(
+ -1,
+ -1,
+ Direction.UP
+ )
+ return false
+ }
+
+ /**
+ * Checks if the enemy has won
+ *
+ * @return True if the enemy has won
+ */
+ fun enemyHasWon(): Boolean {
+ ships.forEach { ship -> if (!ship.isSunken()) return false }
+ return true
+ }
+
+ /**
+ * Shot sent at enemy, checks whether it is a valid shot
+ *
+ * @return True if the shot is valid and can be sent to the enemy
+ */
+ fun shoot(
+ x: Int,
+ y: Int
+ ): Boolean {
+ if (!turn) {
+ return false
+ }
+ val shot =
+ Position2D(
+ x,
+ y
+ )
+ if (!moveValidator.isValidShot(
+ shot,
+ shotsFired
+ )
+ ) return false
+ shotsFired.add(
+ shot
+ )
+ turn = false
+ return true
+ }
+
+ /**
+ * Takes an incoming shot and returns weather the boat was hit, sunken or
+ * the shot missed entirely
+ */
+ fun receiveShot(x: Int, y: Int): ShotOutcome {
+ for (ship in ships) {
+ when (ship.isHit(x, y)) {
+ ShotOutcome.MISS -> {
+ continue
+ }
+ ShotOutcome.HIT -> {
+ shotReceived.add(Pair(Position2D(x, y), ShotOutcome.HIT))
+ return ShotOutcome.HIT
+ }
+ ShotOutcome.SUNKEN -> {
+ shotReceived.removeIf {
+ ship.getPositions().contains(it.first)
+ }
+ ship.getPositions().forEach {
+ shotReceived.add(Pair(it, ShotOutcome.SUNKEN))
+ }
+ return ShotOutcome.SUNKEN
+ }
+ }
+ }
+ shotReceived.add(Pair(Position2D(x, y), ShotOutcome.MISS))
+ return ShotOutcome.MISS
+ }
+
+ /**
+ * Get the outcome of a shot from the communication
+ */
+ fun shotOutcome(x: Int, y: Int, shotOutcome: ShotOutcome) {
+ val shotPosition = Position2D(x, y)
+ shotsFiredWithOutcome.removeIf {
+ it.first == shotPosition
+ }
+ shotsFiredWithOutcome.add(Pair(Position2D(x, y), shotOutcome))
+ if (shotOutcome == ShotOutcome.SUNKEN) {
+ changeHitsToSunken(x, y)
+ }
+ }
+
+ /**
+ * Helper-method: splits ship-string (09U28L...) in parts of 3. Each ship in one element.
+ */
+ fun splitIntoChunks(input: String, chunkSize: Int): List {
+ val chunks = mutableListOf()
+ var index = 0
+ while (index < input.length) {
+ val endIndex = Math.min(index + chunkSize, input.length)
+ chunks.add(input.substring(index, endIndex))
+ index += chunkSize
+ }
+ return chunks
+ }
+
+ /**
+ * This method returns a list of all cells in the game, which are being occupied by the same ship
+ * given as parameters (f.e. 09U) and its length. It is used together with changeHitsToSunken.
+ */
+ fun getShipPositions(ship: String, length: Int): List {
+ val positions = mutableListOf()
+ val x = ship[0].digitToInt()
+ val y = ship[1].digitToInt()
+ val direction = ship[2]
+
+ for (i in 0 until length) {
+ when (direction) {
+ 'U' -> positions.add(Position2D(x, y - i))
+ 'D' -> positions.add(Position2D(x, y + i))
+ 'L' -> positions.add(Position2D(x - i, y))
+ 'R' -> positions.add(Position2D(x + i, y))
+ }
+ }
+ return positions
+ }
+
+ /**
+ * This function is being used to update previous hits on the same boat to sunken.
+ * This is needed to visualize the game properly.
+ */
+
+ fun changeHitsToSunken(x: Int, y: Int) {
+ try {
+ Log.d("changeHitsToSunken", "Starting changeHitsToSunken with coordinates ($x, $y)")
+
+ // Teilt den enemyHash in 3er Gruppen auf
+ val enemyShips = splitIntoChunks(enemyHash ?: "", 3)
+ Log.d("changeHitsToSunken", "Enemy ships split into chunks: $enemyShips")
+
+ var iteration = 0
+ while (iteration < enemyShips.size) {
+ val ship = enemyShips[iteration]
+ Log.d("changeHitsToSunken", "Processing ship: $ship at iteration $iteration")
+
+ // Ermittelt die Positionen des aktuellen Schiffs
+ val shipPositions = getShipPositions(ship, sizes[iteration])
+ Log.d("changeHitsToSunken", "Ship positions: $shipPositions ${sizes[iteration]}")
+
+ // Prüft, ob die übergebene Position Teil dieses Schiffs ist
+ if (shipPositions.contains(Position2D(x, y))) {
+ Log.d("changeHitsToSunken", "Position ($x, $y) is part of ship $ship")
+
+ // Aktualisiert die Positionen des Schiffs auf SUNKEN
+ shipPositions.forEach { pos ->
+ shotsFiredWithOutcome.removeIf { it.first == pos }
+ shotsFiredWithOutcome.add(Pair(pos, ShotOutcome.SUNKEN))
+ Log.d("changeHitsToSunken", "Updated position $pos to SUNKEN")
+ }
+ }
+ iteration += 1
+ }
+ } catch (e: Exception) {
+ Log.d("changeHitsToSunken", "${e}")
+ }
+
+ }
+
+ /**
+ * Serializes the game state information for communication to the frontend
+ */
+ fun serialize(): String {
+ val state = StringBuilder()
+ ships.forEach {
+ state.append(it.getPositions()[0].getXPosition())
+ .append(it.getPositions()[0].getYPosition())
+ .append(it.getDirection())
+ }
+ state.append("^")
+ shotsFiredWithOutcome.forEach {
+ state.append(it.first.getXPosition())
+ .append(it.first.getYPosition())
+ .append(it.second)
+ }
+ state.append("^")
+ shotReceived.forEach {
+ state.append(it.first.getXPosition())
+ .append(it.first.getYPosition())
+ .append(it.second)
+ }
+ return state.toString()
+ }
+
+ fun getShipAtPosition(x: Int, y: Int): Array {
+ val position = Position2D(x, y)
+ ships.forEach { ship ->
+ if (ship.getPositions().contains(position))
+ return ship.getPositions()
+ }
+ return emptyArray()
+ }
+
+ /**
+ * Returns a serialized string of all ship positions
+ */
+ fun getShipPosition(): String {
+ val state = StringBuilder()
+ ships.forEach {
+ state.append(it.getPositions()[0].getXPosition())
+ .append(it.getPositions()[0].getYPosition())
+ .append(it.getDirection())
+ }
+ return state.toString()
+ }
+
+ /**
+ * This method returns a string for all moves the owner of the game has done.
+ */
+ fun shotsFiredWithOutcomeToString(): String {
+ val result = StringBuilder()
+ shotsFiredWithOutcome.forEach { (position, outcome) ->
+ result.append("${position.getXPosition()}${position.getYPosition()}${outcome}")
+ }
+ if (result.isEmpty()) {
+ return "-"
+ }
+ return result.toString()
+ }
+
+ fun isMyTurn(): Boolean {
+ return turn
+ }
+
+ /**
+ * This method returns a string for all moves the peer of the game has done.
+ */
+ fun shotsReceivedWithOutcomeToString(): String {
+ val result = StringBuilder()
+ shotReceived.forEach { (position, outcome) ->
+ result.append("${position.getXPosition()}${position.getYPosition()}${outcome}")
+ }
+ if (result.isEmpty()) {
+ return "-"
+ }
+ return result.toString()
+ }
+
+ /**
+ * This method is used for the spectator mode. It replaces the previous ship's of the owner,
+ * with the correct ships, which the owner has sent via DUELACC.
+ */
+ fun overwriteSpectatorOwnerShip(s: String) {
+ val boats = splitIntoChunks(s, 3)
+ var iterations = 0
+ boats.forEach {
+ ships[iterations] = Ship(sizes[iterations], it.substring(0,1).toInt(), it.substring(1,2).toInt(),
+ Direction.fromString(it.substring(2,3))!!
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/GameStates.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/GameStates.kt
new file mode 100644
index 0000000..648d079
--- /dev/null
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/GameStates.kt
@@ -0,0 +1,22 @@
+package nz.scuttlebutt.tremolavossbol.games.battleships
+
+enum class GameStates(val string: String) {
+ STOPPED ("STOPPED"),
+ INVITED ("INVITED"),
+ WAITING ("WAITING"),
+ WON ("WON"),
+ LOST ("LOST"),
+ SPEC ("SPECTATE"),
+ RUNNING ("RUNNING");
+
+ override fun toString(): String {
+ return this.string
+ }
+
+ /**
+ * This method returns, whether a given GameInstance is still ongoing.
+ */
+ fun isActive(): Boolean {
+ return (this == RUNNING || this == INVITED || this == WAITING || this == SPEC)
+ }
+}
\ No newline at end of file
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/MoveValidator.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/MoveValidator.kt
new file mode 100644
index 0000000..7d5a06d
--- /dev/null
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/MoveValidator.kt
@@ -0,0 +1,100 @@
+package nz.scuttlebutt.tremolavossbol.games.battleships
+
+import android.util.Log
+
+class MoveValidator {
+
+ /**
+ * Checks if an outgoing shot is out of bounds of the board or a duplicate
+ */
+ fun isValidShot(
+ position2D: Position2D,
+ shotsFired: MutableList
+ ): Boolean {
+ if (!inRange(
+ position2D.getXPosition()
+ ) || !inRange(
+ position2D.getYPosition()
+ )
+ ) {
+ return false
+ }
+ shotsFired.forEach { position -> if (position == position2D) return false }
+ return true
+ }
+
+ /**
+ * Checks if an incoming shot is out of bounds of the board
+ */
+ fun isValidShot(
+ position2D: Position2D
+ ): Boolean {
+ if (!inRange(
+ position2D.getXPosition()
+ ) || !inRange(
+ position2D.getYPosition()
+ )
+ ) {
+ return false
+ }
+ return true
+ }
+
+ /**
+ * Checks whether the ships' placement is not out of bounds and does not overlap with existing ships
+ *
+ * @param ships: The array of already placed ships including the newly placed
+ * @param index: The index of the newly placed ship
+ */
+ fun isValidPlacement(
+ ships: Array,
+ index: Int
+ ): Boolean {
+ val placedShip =
+ ships[index]
+ if (placedShip.isOutOfBounds()) {
+ Log.d(
+ "Placed Ship",
+ "${placedShip.getPositions()[0].getXPosition()}, ${placedShip.getPositions()[0].getYPosition()}, ${placedShip.getDirection()} is out of bounds"
+ )
+ return false
+ }
+ var hasOverlap =
+ false
+ ships.forEachIndexed { i, ship ->
+ if (i != index) {
+ hasOverlap = hasOverlap || placedShip.intersects(ship)
+ }
+ }
+ return !hasOverlap
+ }
+
+
+ fun Ship.isOutOfBounds(): Boolean {
+ for (position in this.getPositions()) {
+ if (position.getXPosition() <= 0 || position.getXPosition() >= 9 ||
+ position.getYPosition() <= 0 || position.getYPosition() >= 9) {
+ return true
+ }
+ }
+ return false
+ }
+
+ fun Ship.intersects(other: Ship): Boolean {
+ val thisPositions = this.getPositions().toSet()
+ val otherPositions = other.getPositions().toSet()
+ return thisPositions.intersect(otherPositions).isNotEmpty()
+ }
+
+ /**
+ * Helper function to determine the out-of-bounds coordinates
+ */
+ private fun inRange(
+ position: Int
+ ): Boolean {
+ if (position < 0 || position > 9) {
+ return false
+ }
+ return true
+ }
+}
\ No newline at end of file
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/Position2D.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/Position2D.kt
new file mode 100644
index 0000000..afdf4c9
--- /dev/null
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/Position2D.kt
@@ -0,0 +1,60 @@
+package nz.scuttlebutt.tremolavossbol.games.battleships
+
+/**
+ * This class is a simple 2D vector with limited functionality. It can mainly
+ * hold two integers and check for equality and has some getters and setters.
+ */
+class Position2D(
+ private var x: Int,
+ private var y: Int
+) {
+
+ fun setPosition(
+ x: Int,
+ y: Int
+ ) {
+ this.x =
+ x
+ this.y =
+ y
+ }
+
+ fun getXPosition(): Int {
+ return this.x
+ }
+
+ fun getYPosition(): Int {
+ return this.y
+ }
+
+ fun setXPosition(
+ x: Int
+ ) {
+ this.x =
+ x
+ }
+
+ fun setYPosition(
+ y: Int
+ ) {
+ this.y =
+ y
+ }
+
+ override fun equals(
+ other: Any?
+ ): Boolean {
+ if (other is Position2D) {
+ return (other.getYPosition() == this.y) && (other.getXPosition() == this.x)
+ }
+ return false
+ }
+
+ override fun hashCode(): Int {
+ var result =
+ x
+ result =
+ 31 * result + y
+ return result
+ }
+}
\ No newline at end of file
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/Ship.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/Ship.kt
new file mode 100644
index 0000000..d567a6d
--- /dev/null
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/Ship.kt
@@ -0,0 +1,185 @@
+package nz.scuttlebutt.tremolavossbol.games.battleships
+
+import android.util.Log
+
+/**
+ * This class represents a ship of the battleship game.
+ * It comes with a range of useful functions and keeps track of how many hits it got.
+ *
+ * @param length The length of the ship
+ * @param x The x coordinate of the anchor point of the ship (an outer point)
+ * @param y The y coordinate of the anchor point
+ * @param direction The direction the ship extends in starting from the anchor point
+ */
+class Ship(
+ private val length: Int,
+ x: Int = -1,
+ y: Int = -1,
+ private var direction: Direction = Direction.UP
+) {
+ // Holds the anchor position of the ship
+ private val position =
+ Position2D(
+ x,
+ y
+ )
+
+ // Boolean array, true where the ship got hit
+ private val hits =
+ Array(
+ length
+ ) { false }
+
+ init {
+ if (length <= 0) {
+ throw java.lang.IllegalArgumentException(
+ "Ship length must be greater than zero"
+ )
+ }
+ }
+
+ /**
+ * For a given shot, checks if this ship got hit
+ *
+ * @param x X coordinate of the fired shot
+ * @param y Y coordinate of the fired shot
+ * @return Whether it is hit, sunken or missed
+ */
+ fun isHit(
+ x: Int,
+ y: Int
+ ): ShotOutcome {
+ if ((x != position.getXPosition()) && y != position.getYPosition()) {
+ Log.d(
+ "Is Hit",
+ "${position.getXPosition()}, ${position.getYPosition()}, ${direction}, Total Miss"
+ )
+ return ShotOutcome.MISS
+ }
+ val hitPosition =
+ Position2D(
+ x,
+ y
+ )
+ for (i in 0 until length) {
+ when (direction) {
+ Direction.DOWN -> hitPosition.setYPosition(
+ y - i
+ )
+ Direction.UP -> hitPosition.setYPosition(
+ y + i
+ )
+ Direction.RIGHT -> hitPosition.setXPosition(
+ x - i
+ )
+ Direction.LEFT -> hitPosition.setXPosition(
+ x + i
+ )
+ }
+ if (hitPosition == position) {
+ hits[i] =
+ true
+ if (isSunken()) {
+ return ShotOutcome.SUNKEN
+ }
+ return ShotOutcome.HIT
+ }
+ }
+ return ShotOutcome.MISS
+ }
+
+ /**
+ * Checks if this ship is sunken. It's only sunken if every position of the ship got hit.
+ */
+ fun isSunken(): Boolean {
+ var sunken = true
+ for (i in 0 until length) {
+ sunken = sunken && hits[i]
+ }
+ return sunken
+ }
+
+ /**
+ * Sets the anchor position and the direction of the ship
+ *
+ * @param x The x coordinate of the anchor point
+ * @param y The y coordinate of the anchor point
+ * @param direction The direction the ship will be facing
+ */
+ fun setPosition(
+ x: Int,
+ y: Int,
+ direction: Direction
+ ) {
+ position.setPosition(
+ x,
+ y
+ )
+ this.direction =
+ direction
+ }
+
+ /**
+ * Checks each ship position against each other ship position ot see if they have overlap.
+ * This is really slow but since the ship size is at maximum 5 it should be fine
+ */
+ fun intersects(other: Ship): Boolean {
+ val thisPositions = getPositions()
+ val otherPositions = other.getPositions()
+
+ return thisPositions.any { it in otherPositions }
+ }
+
+ /**
+ * Checks if the ships is partly or fully out of bounds of the 10x10 board
+ *
+ * @return True if it is out of bounds
+ */
+ fun isOutOfBounds(): Boolean {
+ val x = position.getXPosition()
+ val y = position.getYPosition()
+ val gridSize = 10
+
+ if (x < 0 || x >= gridSize || y < 0 || y >= gridSize) {
+ return true
+ }
+
+ return when (direction) {
+ Direction.UP -> y - (length - 1) < 0
+ Direction.DOWN -> y + (length - 1) >= gridSize
+ Direction.RIGHT -> x + (length - 1) >= gridSize
+ Direction.LEFT -> x - (length - 1) < 0
+ }
+ }
+
+ /**
+ * Returns an array of all positions this ships inhabits
+ */
+ fun getPositions(): Array {
+ return Array(length) { i ->
+ getPosition(
+ i
+ )
+ }
+ }
+
+
+ /**
+ * Gets the i-th position of the ship
+ */
+ private fun getPosition(i: Int): Position2D {
+ return when (direction) {
+ Direction.UP -> Position2D(position.getXPosition(), position.getYPosition() - i)
+ Direction.DOWN -> Position2D(position.getXPosition(), position.getYPosition() + i)
+ Direction.RIGHT -> Position2D(position.getXPosition() + i, position.getYPosition())
+ Direction.LEFT -> Position2D(position.getXPosition() - i, position.getYPosition())
+ }
+ }
+
+ /**
+ * Simple getter for the ship direction
+ */
+ fun getDirection(): Direction {
+ return direction
+ }
+}
\ No newline at end of file
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/ShotOutcome.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/ShotOutcome.kt
new file mode 100644
index 0000000..d4bacb8
--- /dev/null
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/battleships/ShotOutcome.kt
@@ -0,0 +1,27 @@
+package nz.scuttlebutt.tremolavossbol.games.battleships
+
+/**
+ * Enum to describe the outcome of a shot in the game
+ */
+enum class ShotOutcome(val string: String) {
+ MISS("M"),
+ HIT("H"),
+ SUNKEN("S");
+
+ override fun toString(): String {
+ return this.string
+ }
+
+ companion object {
+ fun getFromString(s: String): ShotOutcome? {
+ if (s == "M") {
+ return MISS
+ } else if (s == "S") {
+ return SUNKEN
+ } else if (s == "H") {
+ return HIT
+ }
+ return null
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/common/GameInstance.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/common/GameInstance.kt
new file mode 100644
index 0000000..9724e6e
--- /dev/null
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/common/GameInstance.kt
@@ -0,0 +1,52 @@
+package nz.scuttlebutt.tremolavossbol.games.common
+
+import android.util.Log
+import nz.scuttlebutt.tremolavossbol.games.battleships.BattleshipGame
+import nz.scuttlebutt.tremolavossbol.games.battleships.GameStates
+
+/**
+ * This class represents a Instance of a Battleship game. The peerfid is the ID of the opponent.
+ * The state marks the progress in the game. STOPPED is the default state.
+ */
+class GameInstance(gameType: String, fid: String, time: Long, initialState: GameStates) {
+ var game : GameInterface? = null
+ var participantFid : String = "-"
+ var ownerFid : String = "-"
+ var startTime : Long = 0
+ var state : GameStates = initialState
+
+ init {
+ ownerFid = fid
+ startTime = time
+ game = createGameInstance(gameType)
+ if (game == null) {
+ Log.d("GameInstance", "Unknown Game.")
+ }
+ }
+
+ /**
+ * This method creates the instance of the desired game.
+ */
+ private fun createGameInstance(gameType: String): GameInterface? {
+ return when (gameType) {
+ "BSH" -> BattleshipGame()
+ else -> null
+ }
+ }
+
+ override fun toString(): String {
+ return game.toString()
+ }
+
+ fun isActive(): Boolean {
+ return state.isActive()
+ }
+
+ /**
+ * This sets the participant's id
+ */
+ fun setParticipant(part: String) {
+ participantFid = part
+ }
+}
+
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/common/GameInterface.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/common/GameInterface.kt
new file mode 100644
index 0000000..6ae0cfa
--- /dev/null
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/common/GameInterface.kt
@@ -0,0 +1,17 @@
+package nz.scuttlebutt.tremolavossbol.games.common
+
+import nz.scuttlebutt.tremolavossbol.games.battleships.GameStates
+
+/**
+ * This interface defines the necessities for any game.
+ */
+interface GameInterface {
+ var state : GameStates
+ var isRunning : Boolean
+
+ /**
+ * Every Game needs this function returning its identifier.
+ * Make sure the string is unique among all other games.
+ */
+ override fun toString(): String
+}
\ No newline at end of file
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/common/GamesHandler.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/common/GamesHandler.kt
new file mode 100644
index 0000000..2dc7185
--- /dev/null
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/games/common/GamesHandler.kt
@@ -0,0 +1,218 @@
+package nz.scuttlebutt.tremolavossbol.games.common
+
+import android.util.Log
+import android.util.Base64
+import android.webkit.JavascriptInterface
+import nz.scuttlebutt.tremolavossbol.crypto.SSBid
+
+import nz.scuttlebutt.tremolavossbol.games.battleships.BattleshipHandler
+import nz.scuttlebutt.tremolavossbol.games.battleships.BattleshipGame
+import nz.scuttlebutt.tremolavossbol.games.battleships.GameStates
+
+
+/**
+ * This class distributes all requests regarding any games. It is the main access class for any games.
+ * GUI elements request GamesHandler to get overview of current/past games of an user.
+ */
+class GamesHandler(identity: SSBid) {
+ var myId: SSBid = identity
+ var count = 0
+ private val instances: MutableList = mutableListOf()
+ // Currently selected game. Each client can only have one active at a time.
+ private var activeInstance: GameInstance? = null
+ private var battleshipHandler: BattleshipHandler = BattleshipHandler(this)
+
+
+ /**
+ * This method returns the number of pending invites for a specified gamemode.
+ */
+ fun getInviteCount(gameType: String): Int {
+ when (gameType) {
+ "BSH" -> {
+ return battleshipHandler.inviteCounter
+ }
+ }
+ return -1
+ }
+
+ /*
+ * Gets triggered after game was stopped in any shape or form.
+ */
+ fun decInviteCount(gameType: String): Boolean {
+ when (gameType) {
+ "BSH" -> {
+ if (battleshipHandler.inviteCounter == 1) {
+ battleshipHandler.inviteCounter--
+ return true
+ }
+ return false
+ }
+ }
+ return false
+ }
+
+ /**
+ * Gets called after Owner sent an invite himself. Only one game per (owner, peer, game) can be active at a time.
+ */
+ fun incInviteCount(gameType: String): Boolean {
+ when (gameType) {
+ "BSH" -> {
+ if (battleshipHandler.inviteCounter == 0) {
+ battleshipHandler.inviteCounter++
+ return true
+ }
+ return false
+ }
+ }
+ return false
+ }
+
+ /**
+ * Returns an instance of any game (if exist) for given parameters.
+ */
+ fun getInstanceFromFids(gameType: String, oID: String, pID: String, timeStamp: Long): GameInstance? {
+ for (game in instances) {
+ if (game.ownerFid == oID && game.participantFid == pID && game.game.toString() == gameType && game.startTime == timeStamp) {
+ return game
+ }
+ }
+ return null
+ }
+
+ /**
+ * This is only for active games.
+ */
+ fun getInstanceFromFids(gameType: String, oID: String, pID: String): GameInstance? {
+ for (game in instances) {
+ if (game.ownerFid == oID && game.participantFid == pID && game.game.toString() == gameType && game.state.isActive()) {
+ return game
+ }
+ }
+ return null
+ }
+
+ /**
+ * This method is used to get an instance after receiving an invite.
+ */
+ fun getInstanceFromFid(gameType: String, oID: String): GameInstance? {
+ for (game in instances) {
+ if (game.ownerFid == oID && game.game.toString() == gameType && game.participantFid == "-") {
+ return game
+ }
+ }
+ return null
+ }
+
+ /**
+ * This method returns the list of games used to display the current games in GUI.
+ */
+ @JavascriptInterface
+ fun getListOfGames(): Array {
+ val array = Array(instances.size) { "" }
+ for ((index, game) in instances.withIndex()) {
+ array[index] = getInstanceDescriptor(game)
+ }
+ return array
+ }
+
+ /**
+ * This method reacts to incoming (from peers) game commands.
+ */
+ @JavascriptInterface
+ fun onGameBackendEvent(s: String): String {
+ Log.d("GamesHandler Recv", s)
+ val decodedBytes = Base64.decode(s, Base64.DEFAULT)
+ val decodedString = String(decodedBytes)
+ Log.d("GamesHandler Recv", decodedString)
+ val args = decodedString.split(" ")
+ when (args[0]) {
+ "BSH" -> {
+ if (battleshipHandler == null) {
+ battleshipHandler = BattleshipHandler(this)
+ }
+ return battleshipHandler!!.handleRequest(decodedString.substring(4), activeInstance)
+ }
+ else -> {
+ Log.d("GamesHandler", "Unknown Game")
+ return ""
+ }
+ }
+ }
+
+ /**
+ * This method is used to react to incoming messages regarding updating the GUI.
+ */
+ @JavascriptInterface
+ fun getInstanceDescriptorFromFids(gameType: String, oID: String, pID: String, timeStamp: String): String {
+ val instance: GameInstance? = getInstanceFromFids(gameType, oID, pID, timeStamp.toLong())
+ if (instance == null) {
+ return ""
+ } else {
+ return getInstanceDescriptor(instance)
+ }
+ }
+
+ /**
+ * This method is being called, when you want to see the gamescreen. It contains the config of it.
+ */
+ @JavascriptInterface
+ fun getInstanceDescriptorFromFid(gameType: String, oID: String): String {
+ val instance: GameInstance? = getInstanceFromFid(gameType, oID)
+ if (instance == null) {
+ return ""
+ } else {
+ return getInstanceDescriptor(instance)
+ }
+ }
+
+ /**
+ * This method gets called, as soon as you want to open the Game-GUI.
+ */
+ // TODO change depending on game mode
+ private fun getInstanceDescriptor(i: GameInstance): String {
+ var myTurn: String = "0"
+
+ if ((i.game as BattleshipGame).gameState == null) {
+ return "$i ${i.ownerFid} ${i.participantFid} ${i.startTime} ${i.state}"
+ }
+ if ((i.game as BattleshipGame).gameState?.isMyTurn() == true) {
+ myTurn = "1"
+ }
+ val serialized = (i.game as BattleshipGame).serialize()
+ //val recv = (i.game as BattleshipGame).gameState!!.shotsReceivedWithOutcomeToString()
+ //val deliv = (i.game as BattleshipGame).gameState!!.shotsFiredWithOutcomeToString()
+ return "$i ${i.ownerFid} ${i.participantFid} ${i.startTime} ${i.state} $myTurn $serialized"
+ }
+
+ /**
+ * Adds a new gameinstance to the list. Returns the index of the created instance.
+ * You can always create a new game. Limitation only kicks in on participant's side.
+ */
+ fun addOwnGame(gameType: String, ownerFid: String, initialState: GameStates): Int {
+ val currentTime = System.currentTimeMillis()
+ addInstanceToList(GameInstance(gameType, ownerFid, currentTime, initialState))
+ count++
+ return instances.size
+ }
+
+ /**
+ * This method delivers the list of all current games to the frontend on request.
+ */
+ @JavascriptInterface
+ fun createInstanceList(): String {
+ return instances.joinToString(separator = "$") { getInstanceDescriptor(it) }
+ // TODO read out log to retrieve all games
+ }
+
+ private fun addInstanceToList(instance: GameInstance) {
+ instances.add(instance)
+ }
+
+ /**
+ * Compares public key to my public key.
+ */
+ fun isIdEqualToMine(id: String): Boolean {
+ Log.d("BSH isEqualToMine", myId.toRef() + " " + id)
+ return myId.toRef() == id
+ }
+}
\ No newline at end of file
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/utils/Constants.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/utils/Constants.kt
index 12cb4a5..8651da3 100644
--- a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/utils/Constants.kt
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/utils/Constants.kt
@@ -29,7 +29,14 @@ class Constants{
// app name schema
val TINYSSB_APP_ALIAS = Bipf.mkString("ALI") // fid str
val TINYSSB_APP_BOX = Bipf.mkString("BOX") // bytes
- val TINYSSB_APP_BOX2 = Bipf.mkString("BX2") // bytes
+ val TINYSSB_APP_BOX2 = Bipf.mkString("BX2") //
+
+ val TINYSSB_APP_C4_START = Bipf.mkString("C4N") // new str str str str
+ val TINYSSB_APP_C4_END = Bipf.mkString("C4E") // end str str str
+ val TINYSSB_APP_C4_INVITE = Bipf.mkString("C4I") // invite str str
+ val TINYSSB_APP_C4_DECLINE = Bipf.mkString("C4D") // decline str str
+
+ val TINYSSB_APP_GAMETEXT = Bipf.mkString("GAM") // BATTLESHIP
val TINYSSB_APP_KANBAN = Bipf.mkString("KAN") // kanban boards
val TINYSSB_APP_SCHEDULING = Bipf.mkString("SCH") // event scheduling
val TINYSSB_APP_TEXTANDVOICE = Bipf.mkString("TAV") // str bytes int (xref)