Contest 15 - Battleship - Peter Porter
Here is my Battleship entry:
https://jsfiddle.net/dgewaLj0
Looking at my code, you wont see textareas for all the cells that make up the play area grid, because I had the script create them for me. Saved me the pain of manually hardcoding 100 individual textareas.
HTML
Code:
<div class="game-wrapper">
<!-- Left: game board -->
<div>
<h1>Battleship</h1>
<div id="game"></div>
<div id="controls">
<input type="text" id="coordInput" placeholder="e.g. A5" maxlength="3">
<button onclick="fire()">Fire</button>
</div>
<p id="status">Chances left: 40 | Hits: 0 | Misses: 0 | Ships sunk: 0</p>
<button id="restartBtn" style="display:none;" onclick="initGame()">Play Again</button>
</div>
<!-- Right: side column with textarea -->
<div class="side-column">
<textarea>
Objective:
Sink all of the hidden ships on the 10×10 grid before you run out of chances.
Fleet:
There are 5 ships of different lengths. They are placed randomly on the grid, either horizontally or vertically.
Taking a Shot:
Type the coordinate of the cell you want to target into the input box, for example:
A5 ? Column A, Row 5.
Press Enter on your keyboard or click the Fire button.
The cell will update, X ? you hit part of a ship, 0 ? you missed.
Below the input box you’ll see:
• Chances left
• Hits
• Misses
• Ships sunk
</textarea>
</div>
</div>
Style
Code:
body {
font-family: monospace;
text-align: center;
background-color: #f0f8ff;
}
table {
border-collapse: collapse;
margin: auto;
}
td {
padding: 0;
margin: 0;
text-align: center;
}
.row-header {
padding-right: 8px;
min-width: 25px;
text-align: right;
}
textarea {
resize: none;
width: 30px;
height: 30px;
text-align: center;
font-size: 18px;
background-color: #add8e6;
border: none; /* no visible borders */
margin-right: 5px;
margin-bottom: 3px;
}
.miss {
background-color: #add8e6;
color: white;
.hit {
background-color: #add8e6;
color: red;
}
#controls {
margin-top: 20px;
}
.game-wrapper {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 20px;
height: 600px;
}
.side-column {
background-color: #E0E0E0;
border-radius: 8px;
padding: 10px;
margin-top: 81px;
}
.side-column textarea {
background-color: #E0E0E0;
width: 350px;
height: 100%;
resize: none;
font-family: monospace;
font-size: 14px;
box-sizing: border-box;
text-align: left;
}
Javascript
Code:
const SIZE = 10; // Size of the board (10x10 grid)
const CHANCES = 40; // Number of shots the player has
let grid = []; // 2D array representing the board cells
let ships = []; // Array holding all ships and their coordinates
let chancesLeft, hits, misses, sunk; // Counters for game progress
// Initialize a new game
function initGame() {
chancesLeft = CHANCES;
hits = 0;
misses = 0;
sunk = 0;
grid = Array.from({length: SIZE}, () => Array(SIZE).fill("~"));
ships = [];
placeShips();
drawBoard();
updateStatus();
document.getElementById("restartBtn").style.display = "none";
}
// Draw the board as an HTML table
function drawBoard() {
let html = "<table><tr><td></td>";
for (let c=0; c<SIZE; c++) {
html += `<td>${String.fromCharCode(65+c)}</td>`;
}
html += "</tr>";
for (let r=0; r<SIZE; r++) {
html += `<tr><td class="row-header">${r+1}</td>`;
for (let c=0; c<SIZE; c++) {
html += `<td><textarea id="cell-${r}-${c}" readonly>${grid[r][c]}</textarea></td>`;
}
html += "</tr>";
}
html += "</table>";
document.getElementById("game").innerHTML = html;
}
// Update status line
document.getElementById("status").innerText =
`Chances left: ${chancesLeft} | Hits: ${hits} | Misses: ${misses} | Ships sunk: ${sunk}`;
}
// Handle firing at a coordinate
function fire() {
let input = document.getElementById("coordInput").value.trim().toUpperCase();
document.getElementById("coordInput").value = "";
if (!/^[A-J](10|[1-9])$/.test(input)) {
alert("Invalid coordinate! Use format like A5.");
return;
}
let col = input.charCodeAt(0) - 65;
let row = parseInt(input.slice(1)) - 1;
let cell = document.getElementById(`cell-${row}-${col}`);
if (cell.classList.contains("hit") || cell.classList.contains("miss")) {
alert("Already targeted!");
return;
}
let hitShip = ships.find(ship => ship.some(([r,c]) => r===row && c===col));
if (hitShip) {
cell.value = "X";
cell.classList.add("hit");
hits++;
hitShip.hitCount = (hitShip.hitCount || 0) + 1;
if (hitShip.hitCount === hitShip.length) {
sunk++;
alert("You sunk a ship!");
}
chancesLeft--; // <-- lose a chance even on a hit
} else {
cell.value = "0";
cell.classList.add("miss");
misses++;
chancesLeft--;
}
updateStatus();
checkGameOver();
}
// Check win/lose conditions
function checkGameOver() {
let totalShipCells = ships.reduce((sum, s) => sum + s.length, 0);
if (hits === totalShipCells) {
alert("You win!");
document.getElementById("restartBtn").style.display = "inline";
} else if (chancesLeft <= 0) {
alert("Game over! You lose.");
document.getElementById("restartBtn").style.display = "inline";
}
}
// Randomly place ships
function placeShips() {
const shipSizes = [2,3,3,4,5];
for (let size of shipSizes) {
let placed = false;
while (!placed) {
let orientation = Math.random() < 0.5 ? "H" : "V";
let row = Math.floor(Math.random()*SIZE);
let col = Math.floor(Math.random()*SIZE);
let coords = [];
for (let i=0; i<size; i++) {
let r = row + (orientation==="V"?i:0);
let c = col + (orientation==="H"?i:0);
if (r>=SIZE || c>=SIZE) { coords=[]; break; }
coords.push([r,c]);
}
if (coords.length===size && validPlacement(coords)) {
ships.push(coords);
placed = true;
}
}
}
}
// Ensure ships don't overlap or touch
function validPlacement(coords) {
for (let [r,c] of coords) {
for (let dr=-1; dr<=1; dr++) {
for (let dc=-1; dc<=1; dc++) {
let nr=r+dr, nc=c+dc;
if (nr>=0 && nr<SIZE && nc>=0 && nc<SIZE) {
if (ships.some(ship => ship.some(([sr,sc]) => sr===nr && sc===nc))) {
return false;
}
}
}
}
}
return true;
}
// Allow Enter key to fire
document.getElementById("coordInput").addEventListener("keydown", e => {
if (e.key==="Enter") fire();
});
// Start game on load
initGame();
Re: Contest 15 - Battleship - Peter Porter
Shouldn't contest submissions be private until after the contest closes?
This is a hidden post waiting to be moderated. (steve)