parent
08b771c863
commit
7f5010ae19
@ -0,0 +1,81 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Border-radius Previewer</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1>Border-radius Previewer</h1>
|
||||||
|
|
||||||
|
<div id="preview-box"></div>
|
||||||
|
|
||||||
|
<div class="controls-grid">
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="tlh">Top Left (H):</label>
|
||||||
|
<input type="range" id="tlh" min="0" max="200" value="25">
|
||||||
|
<span id="tlh-value">25px</span>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="trh">Top Right (H):</label>
|
||||||
|
<input type="range" id="trh" min="0" max="200" value="25">
|
||||||
|
<span id="trh-value">25px</span>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="brh">Bottom Right (H):</label>
|
||||||
|
<input type="range" id="brh" min="0" max="200" value="25">
|
||||||
|
<span id="brh-value">25px</span>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="blh">Bottom Left (H):</label>
|
||||||
|
<input type="range" id="blh" min="0" max="200" value="25">
|
||||||
|
<span id="blh-value">25px</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="vertical-controls" class="hidden">
|
||||||
|
<hr>
|
||||||
|
<h2>Bonus: Vertical Radius (8-Value Mode)</h2>
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="tlv">Top Left (V):</label>
|
||||||
|
<input type="range" id="tlv" min="0" max="200" value="25">
|
||||||
|
<span id="tlv-value">25px</span>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="trv">Top Right (V):</label>
|
||||||
|
<input type="range" id="trv" min="0" max="200" value="25">
|
||||||
|
<span id="trv-value">25px</span>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="brv">Bottom Right (V):</label>
|
||||||
|
<input type="range" id="brv" min="0" max="200" value="25">
|
||||||
|
<span id="brv-value">25px</span>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="blv">Bottom Left (V):</label>
|
||||||
|
<input type="range" id="blv" min="0" max="200" value="25">
|
||||||
|
<span id="blv-value">25px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="toggle-8-value">
|
||||||
|
<span class="slider round"></span>
|
||||||
|
8-Value Mode
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="output-section">
|
||||||
|
<textarea id="css-output" readonly></textarea>
|
||||||
|
<button id="copy-button">Copy CSS</button>
|
||||||
|
<p id="copy-message" class="hidden">Copied!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// --- Select Elements ---
|
||||||
|
const previewBox = document.getElementById('preview-box');
|
||||||
|
const cssOutput = document.getElementById('css-output');
|
||||||
|
const copyButton = document.getElementById('copy-button');
|
||||||
|
const copyMessage = document.getElementById('copy-message');
|
||||||
|
|
||||||
|
// Get all input elements for radii (Horizontal: h, Vertical: v)
|
||||||
|
const hInputs = ['tlh', 'trh', 'brh', 'blh'].map(id => document.getElementById(id));
|
||||||
|
const vInputs = ['tlv', 'trv', 'brv', 'blv'].map(id => document.getElementById(id));
|
||||||
|
const allInputs = [...hInputs, ...vInputs];
|
||||||
|
|
||||||
|
// Bonus feature elements
|
||||||
|
const toggle8Value = document.getElementById('toggle-8-value');
|
||||||
|
const verticalControls = document.getElementById('vertical-controls');
|
||||||
|
let is8ValueMode = false;
|
||||||
|
|
||||||
|
// --- Core Function: Update Radius ---
|
||||||
|
function updateRadius() {
|
||||||
|
// 1. Get all current radius values
|
||||||
|
const hValues = hInputs.map(input => input.value);
|
||||||
|
const vValues = vInputs.map(input => input.value);
|
||||||
|
|
||||||
|
// Update display spans next to sliders
|
||||||
|
hInputs.forEach((input, index) => {
|
||||||
|
document.getElementById(`${input.id}-value`).textContent = `${hValues[index]}px`;
|
||||||
|
});
|
||||||
|
vInputs.forEach((input, index) => {
|
||||||
|
document.getElementById(`${input.id}-value`).textContent = `${vValues[index]}px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Construct the CSS string
|
||||||
|
const hString = hValues.join('px ') + 'px';
|
||||||
|
|
||||||
|
let cssString;
|
||||||
|
if (is8ValueMode) {
|
||||||
|
const vString = vValues.join('px ') + 'px';
|
||||||
|
cssString = `border-radius: ${hString} / ${vString};`;
|
||||||
|
} else {
|
||||||
|
// Standard 4-value syntax
|
||||||
|
cssString = `border-radius: ${hString};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Apply styles and update output (User Stories 1 & 2)
|
||||||
|
previewBox.style.borderRadius = cssString.replace('border-radius: ', '').replace(';', '');
|
||||||
|
cssOutput.value = cssString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Bonus Feature: Toggle 8-Value Mode ---
|
||||||
|
toggle8Value.addEventListener('change', () => {
|
||||||
|
is8ValueMode = toggle8Value.checked;
|
||||||
|
if (is8ValueMode) {
|
||||||
|
verticalControls.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
verticalControls.classList.add('hidden');
|
||||||
|
}
|
||||||
|
updateRadius();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Event Listeners ---
|
||||||
|
// Update radius whenever any slider moves
|
||||||
|
allInputs.forEach(input => {
|
||||||
|
input.addEventListener('input', updateRadius);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Copy to Clipboard (User Story 3) ---
|
||||||
|
copyButton.addEventListener('click', () => {
|
||||||
|
// Select the text in the textarea
|
||||||
|
cssOutput.select();
|
||||||
|
cssOutput.setSelectionRange(0, 99999); // For mobile devices
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the modern Clipboard API
|
||||||
|
navigator.clipboard.writeText(cssOutput.value).then(() => {
|
||||||
|
copyMessage.classList.remove('hidden');
|
||||||
|
setTimeout(() => {
|
||||||
|
copyMessage.classList.add('hidden');
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback for older browsers
|
||||||
|
document.execCommand('copy');
|
||||||
|
copyMessage.textContent = 'Copied! (Fallback)';
|
||||||
|
copyMessage.classList.remove('hidden');
|
||||||
|
setTimeout(() => {
|
||||||
|
copyMessage.classList.add('hidden');
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize the box on load
|
||||||
|
updateRadius();
|
||||||
|
});
|
||||||
@ -0,0 +1,135 @@
|
|||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
background-color: #f4f4f9;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#preview-box {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
background-color: #007bff;
|
||||||
|
margin: 30px auto;
|
||||||
|
transition: border-radius 0.1s ease;
|
||||||
|
border: 5px solid #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#css-output {
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
resize: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#copy-button {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#copy-button:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bonus Feature Styles */
|
||||||
|
#vertical-controls {
|
||||||
|
grid-column: 1 / -1; /* Span two columns */
|
||||||
|
padding-top: 15px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#copy-message {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle Switch Styles (Optional) */
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 60px;
|
||||||
|
height: 34px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.toggle-switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
transition: 0.4s;
|
||||||
|
border-radius: 34px;
|
||||||
|
}
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: white;
|
||||||
|
transition: 0.4s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: #007bff;
|
||||||
|
}
|
||||||
|
input:checked + .slider:before {
|
||||||
|
transform: translateX(26px);
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Bit Masks Timezone Finder</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="app-container">
|
||||||
|
<h1>🌎 Bit Masks Timezone Finder</h1>
|
||||||
|
|
||||||
|
<div class="controls-panel">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="gmt-input">Enter GMT Offset (e.g., +3 or -4):</label>
|
||||||
|
<input type="number" id="gmt-input" min="-12" max="12" value="2">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-options">
|
||||||
|
<label class="checkbox-container">
|
||||||
|
Search for cities NOT in this offset
|
||||||
|
<input type="checkbox" id="search-not">
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button id="find-cities-btn">Find Cities</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="city-list">
|
||||||
|
<h2>Timezones Defined</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="output-area">
|
||||||
|
<h2>Search Results</h2>
|
||||||
|
<p id="summary-count" class="summary">0 cities found.</p>
|
||||||
|
<ul id="results-list">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Define all cities with their GMT offsets.
|
||||||
|
// The 'mask' property will be calculated based on the bit position.
|
||||||
|
const CITIES_DATA = [
|
||||||
|
{ name: "Moscow", gmtOffset: 3 },
|
||||||
|
{ name: "Paris", gmtOffset: 2 },
|
||||||
|
{ name: "Berlin", gmtOffset: 2 },
|
||||||
|
{ name: "Brussels", gmtOffset: 2 },
|
||||||
|
{ name: "Amsterdam", gmtOffset: 2 },
|
||||||
|
{ name: "Rome", gmtOffset: 2 },
|
||||||
|
{ name: "London", gmtOffset: 1 },
|
||||||
|
{ name: "Dublin", gmtOffset: 1 },
|
||||||
|
{ name: "New York", gmtOffset: -4 },
|
||||||
|
{ name: "Washington, DC", gmtOffset: -4 },
|
||||||
|
{ name: "St. Louis", gmtOffset: -5 },
|
||||||
|
{ name: "Los Angeles", gmtOffset: -7 },
|
||||||
|
{ name: "Tokyo", gmtOffset: 9 },
|
||||||
|
{ name: "Beijing", gmtOffset: 8 },
|
||||||
|
{ name: "Ho Chi Mihn City", gmtOffset: 7 },
|
||||||
|
{ name: "Mumbai", gmtOffset: 5 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Developer Note Implementation ---
|
||||||
|
// Mapping Formula: Bit Position = GMT Offset + 12
|
||||||
|
// Range: GMT +12 (pos 24) to GMT -12 (pos 0)
|
||||||
|
|
||||||
|
// Calculate the Bit Mask for each city and populate the mask field.
|
||||||
|
CITIES_DATA.forEach(city => {
|
||||||
|
const bitPosition = city.gmtOffset + 12;
|
||||||
|
// Use the bitwise left shift (<<) to get the mask (2^bitPosition)
|
||||||
|
city.mask = 1 << bitPosition;
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- DOM Elements ---
|
||||||
|
const gmtInput = document.getElementById('gmt-input');
|
||||||
|
const findButton = document.getElementById('find-cities-btn');
|
||||||
|
const searchNotCheckbox = document.getElementById('search-not');
|
||||||
|
const resultsList = document.getElementById('results-list');
|
||||||
|
const summaryCount = document.getElementById('summary-count');
|
||||||
|
const cityListDiv = document.getElementById('city-list');
|
||||||
|
|
||||||
|
// --- Functions ---
|
||||||
|
|
||||||
|
// 1. Initial population of the city list for reference (User Story 1)
|
||||||
|
function populateCityList() {
|
||||||
|
const ul = document.createElement('ul');
|
||||||
|
CITIES_DATA.forEach(city => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
const gmtSign = city.gmtOffset >= 0 ? '+' : '';
|
||||||
|
li.textContent = `${city.name}: GMT ${gmtSign}${city.gmtOffset}`;
|
||||||
|
ul.appendChild(li);
|
||||||
|
});
|
||||||
|
cityListDiv.appendChild(ul);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Core Search Logic (User Story 3)
|
||||||
|
function findCitiesByGmt() {
|
||||||
|
const searchOffset = parseInt(gmtInput.value);
|
||||||
|
const searchNot = searchNotCheckbox.checked; // Bonus Feature
|
||||||
|
|
||||||
|
// Input validation
|
||||||
|
if (isNaN(searchOffset) || searchOffset < -12 || searchOffset > 12) {
|
||||||
|
alert("Please enter a valid GMT offset between -12 and 12.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the search mask based on the GMT offset
|
||||||
|
const bitPosition = searchOffset + 12;
|
||||||
|
const searchMask = 1 << bitPosition;
|
||||||
|
|
||||||
|
const matches = [];
|
||||||
|
|
||||||
|
CITIES_DATA.forEach(city => {
|
||||||
|
// THE CORE BITWISE OPERATION:
|
||||||
|
// If (city.mask & searchMask) > 0, the bits overlap, meaning the city is in that timezone.
|
||||||
|
const isMatch = (city.mask & searchMask) > 0;
|
||||||
|
|
||||||
|
// Apply the 'Search NOT' logic (Bonus Feature)
|
||||||
|
if (searchNot ? !isMatch : isMatch) {
|
||||||
|
matches.push(city.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
displayResults(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Display Results (User Story 3 & Bonus Feature)
|
||||||
|
function displayResults(matches) {
|
||||||
|
// Update summary count (Bonus Feature)
|
||||||
|
summaryCount.textContent = `${matches.length} cities found.`;
|
||||||
|
|
||||||
|
resultsList.innerHTML = '';
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = 'No cities matched the criteria.';
|
||||||
|
resultsList.appendChild(li);
|
||||||
|
} else {
|
||||||
|
matches.forEach(name => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = name;
|
||||||
|
resultsList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Initialization and Event Listener ---
|
||||||
|
|
||||||
|
populateCityList();
|
||||||
|
findButton.addEventListener('click', findCitiesByGmt);
|
||||||
|
|
||||||
|
// Run an initial search for the default value (GMT +2)
|
||||||
|
findCitiesByGmt();
|
||||||
|
});
|
||||||
@ -0,0 +1,184 @@
|
|||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background-color: #f4f7f6;
|
||||||
|
color: #333;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: #007bff;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 2px solid #eee;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Controls Panel --- */
|
||||||
|
.controls-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gmt-input {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-left: 10px;
|
||||||
|
width: 80px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#find-cities-btn {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#find-cities-btn:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Checkbox (Bonus Feature) */
|
||||||
|
.checkbox-container {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 25px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
user-select: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.checkbox-container input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
.checkmark {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
background-color: #eee;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.checkbox-container:hover input ~ .checkmark {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
.checkbox-container input:checked ~ .checkmark {
|
||||||
|
background-color: #007bff;
|
||||||
|
}
|
||||||
|
.checkmark:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.checkbox-container input:checked ~ .checkmark:after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.checkbox-container .checkmark:after {
|
||||||
|
left: 6px;
|
||||||
|
top: 2px;
|
||||||
|
width: 5px;
|
||||||
|
height: 10px;
|
||||||
|
border: solid white;
|
||||||
|
border-width: 0 3px 3px 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- City List --- */
|
||||||
|
#city-list ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#city-list li {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Output Area --- */
|
||||||
|
.output-area {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #28a745;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#results-list {
|
||||||
|
list-style-type: disc;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#results-list li {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.search-options {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
#city-list ul {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,136 @@
|
|||||||
|
// BattleshipEngine.js
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Game board symbols:
|
||||||
|
* 'W' (Water): Unchecked cell
|
||||||
|
* 'M' (Miss): Missed shot
|
||||||
|
* 'H' (Hit): Hit a ship
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Ship {
|
||||||
|
constructor(name, size) {
|
||||||
|
this.name = name;
|
||||||
|
this.size = size;
|
||||||
|
this.hits = 0;
|
||||||
|
this.isSunk = false;
|
||||||
|
this.coords = []; // Array of {r, c} objects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BattleshipGame {
|
||||||
|
constructor(boardSize = 10) {
|
||||||
|
this.boardSize = boardSize;
|
||||||
|
// Board is initialized to 'W' (Water)
|
||||||
|
this.board = Array(boardSize).fill(0).map(() => Array(boardSize).fill('W'));
|
||||||
|
this.ships = this.placeShips();
|
||||||
|
this.shotsTaken = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
placeShips() {
|
||||||
|
const shipTypes = [
|
||||||
|
{ name: "Carrier", size: 5 },
|
||||||
|
{ name: "Battleship", size: 4 },
|
||||||
|
{ name: "Cruiser", size: 3 },
|
||||||
|
{ name: "Submarine", size: 3 },
|
||||||
|
{ name: "Destroyer", size: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const placedShips = [];
|
||||||
|
// Helper to check if a cell is already part of a ship
|
||||||
|
const isOccupied = (r, c) => this.board[r][c] !== 'W';
|
||||||
|
|
||||||
|
for (const shipType of shipTypes) {
|
||||||
|
let placed = false;
|
||||||
|
while (!placed) {
|
||||||
|
const isHorizontal = Math.random() > 0.5;
|
||||||
|
const rStart = Math.floor(Math.random() * this.boardSize);
|
||||||
|
const cStart = Math.floor(Math.random() * this.boardSize);
|
||||||
|
|
||||||
|
let canPlace = true;
|
||||||
|
const newShip = new Ship(shipType.name, shipType.size);
|
||||||
|
const coords = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < shipType.size; i++) {
|
||||||
|
const r = isHorizontal ? rStart : rStart + i;
|
||||||
|
const c = isHorizontal ? cStart + i : cStart;
|
||||||
|
|
||||||
|
// Check bounds (0 to boardSize-1) and collision
|
||||||
|
if (r >= this.boardSize || c >= this.boardSize || r < 0 || c < 0 || isOccupied(r, c)) {
|
||||||
|
canPlace = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
coords.push({ r, c });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canPlace) {
|
||||||
|
newShip.coords = coords;
|
||||||
|
placedShips.push(newShip);
|
||||||
|
// Mark the board internally for collision check, e.g., using 'S'
|
||||||
|
coords.forEach(coord => this.board[coord.r][coord.c] = 'S');
|
||||||
|
placed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the visible board to 'W' where ships were placed
|
||||||
|
this.board = this.board.map(row => row.map(cell => cell === 'S' ? 'W' : cell));
|
||||||
|
|
||||||
|
return placedShips;
|
||||||
|
}
|
||||||
|
|
||||||
|
shoot(r, c) {
|
||||||
|
this.shotsTaken++;
|
||||||
|
const cell = this.board[r][c];
|
||||||
|
let status = { hit: false, miss: false, sunk: null, message: `Shot at R${r + 1}, C${String.fromCharCode(65 + c)}.` };
|
||||||
|
|
||||||
|
if (cell === 'H' || cell === 'M') {
|
||||||
|
status.message = `You already shot at this location! Try again.`;
|
||||||
|
// Decrement shots taken since it was an invalid shot
|
||||||
|
this.shotsTaken--;
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isHit = false;
|
||||||
|
let hitShip = null;
|
||||||
|
|
||||||
|
// Check if any ship occupies this coordinate
|
||||||
|
for (const ship of this.ships) {
|
||||||
|
// Check ship's internal coordinates array
|
||||||
|
const hitCoordIndex = ship.coords.findIndex(coord => coord.r === r && coord.c === c);
|
||||||
|
if (hitCoordIndex !== -1) {
|
||||||
|
ship.hits++;
|
||||||
|
isHit = true;
|
||||||
|
hitShip = ship;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHit) {
|
||||||
|
this.board[r][c] = 'H';
|
||||||
|
status.hit = true;
|
||||||
|
status.message = `**HIT!** Target destroyed at R${r + 1}, C${String.fromCharCode(65 + c)}.`;
|
||||||
|
|
||||||
|
if (hitShip.hits === hitShip.size) {
|
||||||
|
hitShip.isSunk = true;
|
||||||
|
status.sunk = hitShip.name;
|
||||||
|
status.message = `💥 **SUNK!** You destroyed the ${hitShip.name}!`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.board[r][c] = 'M';
|
||||||
|
status.miss = true;
|
||||||
|
status.message = `**MISS.** Water at R${r + 1}, C${String.fromCharCode(65 + c)}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
isGameOver() {
|
||||||
|
return this.ships.every(ship => ship.isSunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBoard() {
|
||||||
|
return this.board;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BattleshipGame;
|
||||||
@ -0,0 +1,172 @@
|
|||||||
|
// index.js
|
||||||
|
|
||||||
|
const Discord = require('discord.js');
|
||||||
|
const BattleshipGame = require('./BattleshipEngine');
|
||||||
|
|
||||||
|
// --- Configuration ---
|
||||||
|
const TOKEN = 'YOUR_BOT_TOKEN_HERE'; // !!! REPLACE ME !!!
|
||||||
|
const PREFIX = 'bb';
|
||||||
|
// Using v12/v13 structure for compatibility.
|
||||||
|
const client = new Discord.Client();
|
||||||
|
|
||||||
|
// Map to hold active game instances: Key: UserID, Value: BattleshipGame object
|
||||||
|
const activeGames = new Map();
|
||||||
|
|
||||||
|
// Emojis for the visual board (Bonus Feature 2)
|
||||||
|
const EMOJIS = {
|
||||||
|
MISS: '⚪', // Missed shot
|
||||||
|
HIT: '🔴', // Hit ship
|
||||||
|
WATER: '🟦', // Unchecked water
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Helper Functions ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Discord Embed for displaying the board graphically. (Bonus Feature 2)
|
||||||
|
*/
|
||||||
|
function createBoardEmbed(game, status) {
|
||||||
|
const board = game.getBoard();
|
||||||
|
let boardString = '';
|
||||||
|
|
||||||
|
// Create column headers (A B C D...)
|
||||||
|
const colHeaders = ' ' + [...'ABCDEFGHIJ'].join(' ');
|
||||||
|
boardString += colHeaders + '\n';
|
||||||
|
|
||||||
|
for (let r = 0; r < board.length; r++) {
|
||||||
|
// Add row number (1-10)
|
||||||
|
boardString += `${r + 1} `;
|
||||||
|
for (let c = 0; c < board[r].length; c++) {
|
||||||
|
const cell = board[r][c];
|
||||||
|
if (cell === 'H') boardString += EMOJIS.HIT;
|
||||||
|
else if (cell === 'M') boardString += EMOJIS.MISS;
|
||||||
|
else boardString += EMOJIS.WATER;
|
||||||
|
boardString += ' '; // Add space for better emoji layout
|
||||||
|
}
|
||||||
|
boardString += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusColor = status.hit ? '#FF0000' : (status.miss ? '#007BFF' : '#343A40');
|
||||||
|
|
||||||
|
const embed = new Discord.MessageEmbed()
|
||||||
|
.setTitle('⚓ Battleship Board ⚓')
|
||||||
|
// Use the board as the main description
|
||||||
|
.setDescription(boardString)
|
||||||
|
.addField('Shots Taken', game.shotsTaken.toString(), true)
|
||||||
|
.addField('Last Action', status.message, true)
|
||||||
|
.setColor(statusColor)
|
||||||
|
.setFooter(`Use "${PREFIX} shoot R,C" (e.g., 5,C) to fire.`);
|
||||||
|
|
||||||
|
return embed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a help message with rules and commands. (User Story 1)
|
||||||
|
*/
|
||||||
|
function sendHelpEmbed() {
|
||||||
|
return new Discord.MessageEmbed()
|
||||||
|
.setTitle('Battleship Bot Commands')
|
||||||
|
.setDescription('Your goal is to sink all 5 opponent ships on a 10x10 grid by guessing their coordinates.')
|
||||||
|
.setColor('#FFA500')
|
||||||
|
.addField('Commands:',
|
||||||
|
`**\`${PREFIX} start\`**: Starts a new game and places ships.\n` +
|
||||||
|
`**\`${PREFIX} shoot R,C\`**: Target a cell. R=Row (1-10), C=Column (A-J). Example: \`${PREFIX} shoot 5,C\`\n` +
|
||||||
|
`**\`${PREFIX} help\`**: Displays this message.\n` +
|
||||||
|
`**\`${PREFIX} surrender\`**: Ends your current game.`, // Bonus Feature 1
|
||||||
|
false)
|
||||||
|
.addField('Board Key:',
|
||||||
|
`${EMOJIS.WATER} = Water\n${EMOJIS.MISS} = Miss\n${EMOJIS.HIT} = Hit`,
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Discord Event Listeners ---
|
||||||
|
|
||||||
|
client.on('ready', () => {
|
||||||
|
console.log(`Logged in as ${client.user.tag}!`);
|
||||||
|
client.user.setActivity(`${PREFIX} help`, { type: 'PLAYING' });
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('message', message => {
|
||||||
|
// Ignore messages from bots and non-command messages
|
||||||
|
if (message.author.bot || !message.content.startsWith(PREFIX)) return;
|
||||||
|
|
||||||
|
// Parse command: bb shoot 5,C -> command='shoot', args=['5,C']
|
||||||
|
const args = message.content.slice(PREFIX.length).trim().split(/ +/);
|
||||||
|
const command = args.shift().toLowerCase();
|
||||||
|
const userId = message.author.id;
|
||||||
|
|
||||||
|
// --- Command Routing ---
|
||||||
|
|
||||||
|
// 1. bb help (User Story 1)
|
||||||
|
if (command === 'help') {
|
||||||
|
message.channel.send(sendHelpEmbed());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. bb start (User Story 2)
|
||||||
|
if (command === 'start') {
|
||||||
|
if (activeGames.has(userId)) {
|
||||||
|
return message.reply(`You already have a game running! Use \`${PREFIX} surrender\` to quit the current one.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newGame = new BattleshipGame();
|
||||||
|
activeGames.set(userId, newGame);
|
||||||
|
message.reply('Game started! 🚀 All 5 ships have been placed.');
|
||||||
|
|
||||||
|
// Send initial board
|
||||||
|
const initialStatus = { hit: false, miss: false, message: "Welcome to the game! Start firing." };
|
||||||
|
message.channel.send(createBoardEmbed(newGame, initialStatus));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. bb surrender (Bonus Feature 1)
|
||||||
|
if (command === 'surrender') {
|
||||||
|
if (activeGames.delete(userId)) {
|
||||||
|
message.reply(`You surrendered. The game has been ended.`);
|
||||||
|
} else {
|
||||||
|
message.reply("You don't have an active game to surrender.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Commands requiring an active game ---
|
||||||
|
if (!activeGames.has(userId)) {
|
||||||
|
return message.reply(`You need to start a game first! Use \`${PREFIX} start\`.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const game = activeGames.get(userId);
|
||||||
|
|
||||||
|
// 4. bb shoot R,C (User Stories 3, 4, 5)
|
||||||
|
if (command === 'shoot') {
|
||||||
|
if (args.length === 0) {
|
||||||
|
return message.reply('Please provide coordinates in the format R,C (e.g., 5,C).');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [coordR, coordC] = args[0].split(',');
|
||||||
|
const r = parseInt(coordR) - 1; // Row is 0-indexed internally (1-10)
|
||||||
|
// Convert 'A' to 0, 'B' to 1, etc.
|
||||||
|
const c = coordC ? coordC.toUpperCase().charCodeAt(0) - 'A'.charCodeAt(0) : NaN;
|
||||||
|
|
||||||
|
if (isNaN(r) || r < 0 || r >= game.boardSize || isNaN(c) || c < 0 || c >= game.boardSize) {
|
||||||
|
return message.reply('Invalid coordinates. Row must be 1-10, Column must be A-J.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the shot and get status
|
||||||
|
const status = game.shoot(r, c);
|
||||||
|
|
||||||
|
// Display board and status (User Story 4 & Bonus Feature 2)
|
||||||
|
message.channel.send(createBoardEmbed(game, status));
|
||||||
|
|
||||||
|
// Check for game over (User Story 5)
|
||||||
|
if (game.isGameOver()) {
|
||||||
|
message.reply(`🏆 **GAME OVER! CONGRATULATIONS!** You sunk all ships in ${game.shotsTaken} shots!`);
|
||||||
|
activeGames.delete(userId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for unrecognized commands
|
||||||
|
message.reply(`Unrecognized command. Type \`${PREFIX} help\` for commands.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.login(TOKEN);
|
||||||
Loading…
Reference in new issue