Major changes

- Improved Smart AI
- Minor design improvements
This commit is contained in:
MaciejkaG 2024-04-15 20:49:19 +02:00
parent fd28365c98
commit 5c937cb9b8
8 changed files with 125 additions and 24 deletions

View File

@ -29,7 +29,6 @@ fs.readFile(path.join(__dirname, 'package.json'), function (err, data) {
packageJSON = JSON.parse(data); packageJSON = JSON.parse(data);
}); });
const app = express(); const app = express();
const flags = process.env.flags ? process.env.flags.split(",") : null; const flags = process.env.flags ? process.env.flags.split(",") : null;
@ -1122,7 +1121,7 @@ io.on('connection', async (socket) => {
await GInfo.passTurn(socket); await GInfo.passTurn(socket);
[posX, posY] = await GInfo.makeAIMove(socket, playerGame.difficulty); [posX, posY] = await GInfo.makeAIMove(socket, playerGame.data.difficulty);
hit = await GInfo.shootShip(socket, 0, posX, posY); hit = await GInfo.shootShip(socket, 0, posX, posY);
@ -1236,6 +1235,12 @@ async function finishPrepPhase(socket, playerGame) {
const socket = io.sockets.sockets.get(sid); const socket = io.sockets.sockets.get(sid);
let placedShips = await GInfo.depleteShips(socket); let placedShips = await GInfo.depleteShips(socket);
if (!placedShips) {
io.to(playerGame.id).emit('toast', "An error occured while autoplacing player's ships");
endGame(playerGame.id);
return;
}
placedShips.forEach(shipData => { placedShips.forEach(shipData => {
socket.emit("placed ship", shipData) socket.emit("placed ship", shipData)
}); });
@ -1249,6 +1254,8 @@ async function finishPrepPhase(socket, playerGame) {
GInfo.timer(playerGame.id, 30, () => { GInfo.timer(playerGame.id, 30, () => {
AFKEnd(playerGame.id); AFKEnd(playerGame.id);
}); });
return true;
} }
async function placeAIShips(socket, playerGame) { async function placeAIShips(socket, playerGame) {

22
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "statki-backend", "name": "statki",
"version": "1.0.0", "version": "0.7.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "statki-backend", "name": "statki",
"version": "1.0.0", "version": "0.7.5",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"connect-redis": "^7.1.1", "connect-redis": "^7.1.1",
@ -550,16 +550,16 @@
} }
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.18.3", "version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==", "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.5.0", "cookie": "0.6.0",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@ -662,9 +662,9 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}, },
"node_modules/express/node_modules/cookie": { "node_modules/express/node_modules/cookie": {
"version": "0.5.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "statki-backend", "name": "statki",
"version": "0.7.4", "version": "0.7.5",
"description": "Backend do gry w statki", "description": "Backend do gry w statki",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
@ -10,7 +10,7 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/MaciejkaG/statki-backend.git" "url": "git+https://github.com/MaciejkaG/statki.git"
}, },
"author": "Maciejka", "author": "Maciejka",
"license": "ISC", "license": "ISC",

View File

@ -5,6 +5,7 @@
"background_color": "black", "background_color": "black",
"theme_color": "black", "theme_color": "black",
"orientation": "any", "orientation": "any",
"display": "minimal-ui",
"icons": [ "icons": [
{ {
"src": "/assets/img/statki-logo-crop.png", "src": "/assets/img/statki-logo-crop.png",
@ -72,6 +73,5 @@
"form_factor": "narrow", "form_factor": "narrow",
"label": "Create game screen" "label": "Create game screen"
} }
], ]
"display": "standalone"
} }

View File

@ -1,4 +1,4 @@
const statki = "statki-by-maciejka-0.7.4"; const statki = "statki-by-maciejka-0.7.5";
const assets = [ const assets = [
"/favicon.ico", "/favicon.ico",
"/assets/css/landing.css", "/assets/css/landing.css",

View File

@ -196,9 +196,10 @@ joinForm.addEventListener('submit', (e) => {
}); });
const pveForm = document.getElementById('pveCreateForm'); const pveForm = document.getElementById('pveCreateForm');
const pveDifficulty = document.getElementById('pveDifficulty').value; const pveDifficultyElem = document.getElementById('pveDifficulty');
pveForm.addEventListener('submit', (e) => { pveForm.addEventListener('submit', (e) => {
const pveDifficulty = pveDifficultyElem.value;
e.preventDefault(); e.preventDefault();
if (pveDifficulty) { if (pveDifficulty) {
lockUI(true); lockUI(true);

View File

@ -183,6 +183,10 @@ export class GameInfo {
const rPos = search[Math.floor(Math.random() * search.length)]; const rPos = search[Math.floor(Math.random() * search.length)];
if (rPos == null) {
return false;
}
placedShips.push({ type: i, posX: rPos.posX, posY: rPos.posY, rot: rPos.rot }); placedShips.push({ type: i, posX: rPos.posX, posY: rPos.posY, rot: rPos.rot });
await this.redis.json.arrAppend(key, `.boards[${playerIdx}].ships`, { type: i, posX: rPos.posX, posY: rPos.posY, rot: rPos.rot, hits: Array.from(new Array(i + 1), () => false) }); await this.redis.json.arrAppend(key, `.boards[${playerIdx}].ships`, { type: i, posX: rPos.posX, posY: rPos.posY, rot: rPos.rot, hits: Array.from(new Array(i + 1), () => false) });
let multips; let multips;
@ -287,15 +291,87 @@ export class GameInfo {
} }
async makeAIMove(socket, difficulty) { async makeAIMove(socket, difficulty) {
difficulty = 0; if (difficulty === 2) {
difficulty = 1;
}
const gameId = socket.session.activeGame; const gameId = socket.session.activeGame;
const key = `game:${gameId}`; const key = `game:${gameId}`;
const boards = await this.redis.json.get(key, { path: `.boards` }); const boards = await this.redis.json.get(key, { path: `.boards` });
if (difficulty == 1) { // If difficulty mode is set to smart, check if there are any shot but not sunk ships if (difficulty == 1) { // If difficulty mode is set to smart, check if there are any shot but not sunk ships
// Iterate through player's ships
for (let i = 0; i < boards[0].ships.length; i++) {
const ship = boards[0].ships[i];
// If the ship has at least one hit field and at least one not hit field
if (ship.hits.includes(false) && ship.hits.includes(true)) {
// Iterate through ships
for (let fieldIdx = 0; fieldIdx < ship.hits.length; fieldIdx++) {
// If the ship we're currently iterating has been hit...
if (ship.hits[fieldIdx]) {
let multips;
switch (ship.rot) { // Set up proper multipliers for each possible rotation
case 0:
multips = [1, 0];
break;
case 1:
multips = [0, 1];
break;
case 2:
multips = [-1, 0];
break;
case 3:
multips = [0, -1];
break;
}
// hitFieldX and hitFieldY simply contain the exact coordinates of the hit field of the ship on the board
let hitFieldX = clamp(ship.posX + multips[0] * fieldIdx, 0, 9);
let hitFieldY = clamp(ship.posY + multips[1] * fieldIdx, 0, 9);
// subtrahents array contains sets of difference factors from the hit field.
// We will use them to target fields around the field that was already hit
// They are similarto the ones used in validateShipPosition(), but shorter
// This is because we do not want to target fields that touch corners with our hit field, but the ones that touch with sides
let subtrahents = [[0, 1], [1, 0], [0, -1], [-1, 0]];
// Shuffle them, so they are later iterated in random order
shuffle(subtrahents);
// Iterate through all subtrahents
for (let j = 0; j < subtrahents.length; j++) {
const subs = subtrahents[j];
// Calculate the target field based on the current set of subtrahents, then clamp it so it doesn't exceed board's boundaries
let targetX = clamp(hitFieldX - subs[0], 0, 9);
let targetY = clamp(hitFieldY - subs[1], 0, 9);
// If the bot has hit two fields of the ship already, lock axises depending on the rotation of the ship
// This makes it so if the bot has hit two out of four fields of a ship that's placed horizontally, it won't shoot above the ship as the ships are always a straight line
if (ship.hits.filter(value => value === true).length >= 2) {
if (!ship.rot % 2) {
targetY = hitFieldY;
} else {
targetX = hitFieldX;
}
}
let shot = boards[0].shots.find((shot) => shot.posX === targetX && shot.posY === targetY);
// If shot == null then the field with coordinates posX and posY was not shot at yet
if (!shot) {
// If the field has not been shot yet and it seems possible, try it!
return [targetX, targetY];
}
}
}
}
}
}
} }
if (difficulty != 2) { // If difficulty mode is not set to Overkill if (difficulty != 2) { // If difficulty mode is not set to Overkill
@ -306,12 +382,11 @@ export class GameInfo {
while (!foundAppropriateTarget) { // As long as no appropriate target was found while (!foundAppropriateTarget) { // As long as no appropriate target was found
[posX, posY] = [Math.floor(Math.random() * 10), Math.floor(Math.random() * 10)]; // Randomise another set of coordinates [posX, posY] = [Math.floor(Math.random() * 10), Math.floor(Math.random() * 10)]; // Randomise another set of coordinates
// let check = checkHit(boards[0].ships, posX, posY);
let shot = boards[0].shots.find((shot) => shot.posX === posX && shot.posY === posY); let shot = boards[0].shots.find((shot) => shot.posX === posX && shot.posY === posY);
// If shot == null then the field with coordinates posX and posY was not shot at yet // If shot == null then the field with coordinates posX and posY was not shot at yet
if (!shot) { if (!shot) {
if (difficulty == 1) { // If difficulty mode is set to smart, check if the shot wasn't near any sunk ship if (difficulty == 1) { // If difficulty mode is set to smart, check if the shot wasn't near any sunk ship (not done yet)
foundAppropriateTarget = true; foundAppropriateTarget = true;
} else { // If difficulty mode is set to simple, just accept that field } else { // If difficulty mode is set to simple, just accept that field
foundAppropriateTarget = true; foundAppropriateTarget = true;
@ -566,6 +641,18 @@ function findEmptyFields(grid, len) { // Find all empty fields in the board
return shipPlacements; return shipPlacements;
} }
function shuffle(array) {
let currentIndex = array.length;
while (currentIndex != 0) {
let randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}
}
function clamp(n, min, max) { function clamp(n, min, max) {
return Math.min(Math.max(n, min), max); return Math.min(Math.max(n, min), max);
} }

View File

@ -20,8 +20,14 @@
<script src="https://unpkg.com/tippy.js@6"></script> <script src="https://unpkg.com/tippy.js@6"></script>
<link rel="stylesheet" href="https://unpkg.com/tippy.js@6/animations/shift-toward-subtle.css" /> <link rel="stylesheet" href="https://unpkg.com/tippy.js@6/animations/shift-toward-subtle.css" />
<link rel="stylesheet" href="https://unpkg.com/tippy.js@6/themes/translucent.css" /> <link rel="stylesheet" href="https://unpkg.com/tippy.js@6/themes/translucent.css" />
<link rel="manifest" href="/app/manifest.json" /> <link rel="manifest" href="/app/manifest.json" />
<meta name="theme-color" content="#000000"/> <meta name="theme-color" content="#000000"/>
<meta name="description" content="The #1 online multiplayer battleships game">
<meta name="keywords" content="battleships, statki, online, multiplayer">
<meta name="author" content="Maciejka">
</head> </head>
<body> <body>
<span class="designerTag">Designed by Maciejka</span> <span class="designerTag">Designed by Maciejka</span>