Skip to content

Instantly share code, notes, and snippets.

@redconfetti
Last active September 3, 2026 16:23
Show Gist options
  • Select an option

  • Save redconfetti/a3f4561e15820fe77e63e4168adcc7a2 to your computer and use it in GitHub Desktop.

Select an option

Save redconfetti/a3f4561e15820fe77e63e4168adcc7a2 to your computer and use it in GitHub Desktop.
Code Name: Sector emulation

Code Name: Sector

An HTML/JavaScript emulation of the 1977 Milton-Bradley electronic board game that runs in Chrome or Firefox. Based on the TI-99/4A BASIC version, generated by Replit.

Created as a demo for my friend Bryant Bonet.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Code Name: Sector</title>
<style>
:root {
--ink: #eaf5ff;
--muted: #91a8bb;
--panel: #102231;
--panel-2: #172e40;
--line: #2d526b;
--cyan: #45e0e8;
--green: #65e5a0;
--yellow: #ffd166;
--red: #ff667a;
--orange: #ff9d52;
--shadow: 0 18px 50px rgba(0, 0, 0, .35);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
color: var(--ink);
background:
radial-gradient(circle at 80% -10%, rgba(60, 139, 166, .28), transparent 35rem),
linear-gradient(135deg, #07131e 0%, #0c1c2a 48%, #07131e 100%);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
letter-spacing: .02em;
}
button, input, select { font: inherit; }
button {
border: 1px solid var(--line);
border-radius: 8px;
padding: .72rem .8rem;
color: var(--ink);
background: #193649;
cursor: pointer;
transition: transform .12s ease, background .12s ease, border-color .12s ease;
}
button:hover:not(:disabled) { transform: translateY(-1px); background: #24516a; border-color: #5f9bb3; }
button:active:not(:disabled) { transform: translateY(0); }
button:disabled { cursor: not-allowed; opacity: .35; }
button.primary { color: #062029; background: var(--cyan); border-color: var(--cyan); font-weight: 800; }
button.primary:hover:not(:disabled) { background: #86f2f0; border-color: #86f2f0; }
button.warn { color: #281d04; background: var(--yellow); border-color: var(--yellow); }
button.danger { color: #2a0911; background: var(--red); border-color: var(--red); }
button.subtle { background: transparent; }
.shell { width: min(1180px, calc(100% - 2rem)); margin: 0 auto; padding: 2rem 0 3rem; }
header { display: flex; align-items: end; justify-content: space-between; gap: 1rem; margin-bottom: 1.3rem; }
.kicker { color: var(--cyan); font-size: .78rem; letter-spacing: .18em; text-transform: uppercase; }
h1 { margin: .35rem 0 0; font-size: clamp(1.7rem, 4vw, 3.1rem); letter-spacing: .08em; }
.subtitle { max-width: 42rem; color: var(--muted); line-height: 1.6; font-size: .9rem; }
.screen {
min-height: 34rem;
border: 1px solid #35627a;
border-radius: 16px;
background: rgba(10, 25, 37, .82);
box-shadow: var(--shadow);
overflow: hidden;
}
.screen-bar {
display: flex; justify-content: space-between; align-items: center; gap: 1rem;
padding: .7rem 1rem; border-bottom: 1px solid var(--line); background: rgba(20, 48, 66, .8);
font-size: .74rem; color: var(--muted); text-transform: uppercase; letter-spacing: .12em;
}
.lamp { display: inline-flex; align-items: center; gap: .45rem; }
.lamp::before { content: ""; width: .55rem; height: .55rem; border-radius: 50%; background: var(--green); box-shadow: 0 0 12px var(--green); }
.lamp.off::before { background: #506879; box-shadow: none; }
.setup { max-width: 760px; margin: 0 auto; padding: clamp(1.5rem, 5vw, 4rem); }
.setup h2 { margin: 0 0 .6rem; font-size: 1.35rem; }
.setup p { color: var(--muted); line-height: 1.65; font-size: .88rem; }
form .field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
label { display: grid; gap: .4rem; color: var(--muted); font-size: .78rem; }
input, select {
width: 100%; border: 1px solid var(--line); border-radius: 7px; padding: .72rem .75rem;
color: var(--ink); background: #0b1c2b; outline: none;
}
input:focus, select:focus { border-color: var(--cyan); box-shadow: 0 0 0 3px rgba(69, 224, 232, .13); }
.check-row { display: flex; flex-wrap: wrap; gap: 1rem; margin: 1.2rem 0 1.5rem; }
.check { display: flex; align-items: center; gap: .55rem; color: var(--ink); cursor: pointer; }
.check input { width: auto; accent-color: var(--cyan); }
.form-actions { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center; }
.hint { margin: 0; font-size: .74rem; color: var(--muted); }
.game { padding: 1rem; }
.dashboard { display: grid; grid-template-columns: 1.15fr .85fr; gap: 1rem; }
.panel {
border: 1px solid var(--line); border-radius: 11px; background: rgba(18, 43, 59, .72); overflow: hidden;
}
.panel-title {
padding: .65rem .8rem; border-bottom: 1px solid var(--line); color: var(--muted);
font-size: .7rem; letter-spacing: .12em; text-transform: uppercase;
}
.panel-body { padding: .9rem; }
.readout {
display: grid; grid-template-columns: repeat(4, 1fr); gap: .6rem; margin-bottom: .9rem;
}
.readout-cell { min-width: 0; padding: .7rem .5rem; border-radius: 7px; background: #091a28; text-align: center; }
.readout-label { display: block; color: var(--muted); font-size: .65rem; }
.readout-value { display: block; margin-top: .35rem; color: var(--cyan); font-size: 1.35rem; font-weight: 800; }
.status {
min-height: 3.15rem; display: flex; align-items: center; justify-content: center; text-align: center;
padding: .75rem; border: 1px solid #2f7184; border-radius: 7px; color: var(--cyan);
background: #08212e; font-weight: 800; letter-spacing: .14em;
}
.status.good { color: var(--green); border-color: #398b67; background: #09271f; }
.status.warn { color: var(--yellow); border-color: #987633; background: #2a210e; }
.status.bad { color: var(--red); border-color: #8d3c4b; background: #2b1018; }
.status.secret { color: var(--orange); border-color: #925b36; background: #281a10; }
.status small { display: block; margin-top: .35rem; color: inherit; opacity: .7; font-size: .65rem; letter-spacing: .04em; font-weight: 400; }
.compass-wrap { display: grid; place-items: center; padding: .65rem 0 1rem; }
.compass { position: relative; width: 184px; height: 184px; border: 1px solid #3a6c84; border-radius: 50%; background: radial-gradient(circle, #123449, #0a1c2b 68%); }
.compass::before, .compass::after { content: ""; position: absolute; background: #2b576c; }
.compass::before { width: 1px; top: 8px; bottom: 8px; left: 50%; }
.compass::after { height: 1px; left: 8px; right: 8px; top: 50%; }
.needle { position: absolute; left: 50%; top: 50%; width: 2px; height: 70px; transform-origin: 50% 100%; transform: translate(-50%, -100%) rotate(0deg); background: var(--cyan); box-shadow: 0 0 11px var(--cyan); }
.needle::after { content: ""; position: absolute; bottom: -3px; left: -4px; width: 10px; height: 10px; border-radius: 50%; background: var(--cyan); }
.dir { position: absolute; transform: translate(-50%, -50%); color: var(--muted); font-size: .72rem; }
.dir.n { left: 50%; top: 7%; } .dir.ne { left: 82%; top: 18%; } .dir.e { left: 93%; top: 50%; }
.dir.se { left: 82%; top: 82%; } .dir.s { left: 50%; top: 93%; } .dir.sw { left: 18%; top: 82%; }
.dir.w { left: 7%; top: 50%; } .dir.nw { left: 18%; top: 18%; }
.compass-caption { text-align: center; color: var(--muted); font-size: .72rem; }
.board-wrap { padding: .55rem .7rem .75rem; }
#boardCanvas {
display: block; width: 100%; height: auto; min-height: 260px; border: 1px solid #2a5168;
border-radius: 7px; background: #071823;
}
.board-legend {
display: flex; flex-wrap: wrap; gap: .75rem 1.2rem; margin-top: .65rem;
color: var(--muted); font-size: .68rem;
}
.legend-item { display: inline-flex; align-items: center; gap: .4rem; }
.legend-swatch { width: .7rem; height: .7rem; border-radius: 50%; background: var(--cyan); }
.legend-swatch.ship-red { background: #ff667a; }
.legend-swatch.ship-green { background: #65e5a0; }
.legend-swatch.ship-blue { background: #5aa9ff; }
.legend-swatch.ship-yellow { background: #ffd166; }
.legend-swatch.range { border: 1px dashed var(--yellow); border-radius: 2px; background: transparent; }
.legend-swatch.hidden-contact { border: 1px dashed var(--orange); border-radius: 2px; background: transparent; }
.legend-swatch.limit { width: 1rem; height: 0; border-top: 2px dashed var(--red); border-radius: 0; background: transparent; }
.control-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .6rem; }
.control-grid button { min-height: 3.1rem; }
.control-grid .wide { grid-column: span 2; }
.section-gap { margin-top: 1rem; }
.depth-form { display: none; gap: .65rem; align-items: end; margin-top: .8rem; padding: .8rem; border: 1px solid #926f2d; border-radius: 7px; background: rgba(68, 48, 13, .35); }
.depth-form.visible { display: flex; }
.depth-form label { flex: 1; color: var(--yellow); }
.depth-form button { flex: 0 0 auto; }
.fleet { display: grid; gap: .55rem; }
.ship-card {
display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .65rem;
padding: .65rem .7rem; border: 1px solid transparent; border-radius: 7px; background: rgba(7, 23, 35, .7);
}
.ship-card.active { border-color: var(--cyan); background: rgba(18, 65, 79, .65); }
.ship-badge { display: grid; place-items: center; width: 2rem; height: 2rem; border-radius: 5px; color: #062029; background: var(--cyan); font-weight: 900; }
.ship-name { font-size: .82rem; font-weight: 700; }
.ship-meta { margin-top: .2rem; color: var(--muted); font-size: .68rem; }
.turn-tag { color: var(--yellow); font-size: .66rem; }
.rules { color: var(--muted); font-size: .73rem; line-height: 1.65; }
.rules strong { color: var(--ink); font-weight: 600; }
.log { max-height: 9.5rem; overflow: auto; padding: .8rem; color: var(--muted); font-size: .7rem; line-height: 1.7; }
.log div { border-bottom: 1px solid rgba(45, 82, 107, .35); padding: .18rem 0; }
.log div:last-child { border: 0; }
.log .important { color: var(--yellow); }
.log .success { color: var(--green); }
.log .failure { color: var(--red); }
.footer-note { margin-top: 1rem; color: var(--muted); font-size: .7rem; line-height: 1.55; }
.manual-panel { margin-top: 1rem; border: 1px solid var(--line); border-radius: 16px; background: rgba(10, 25, 37, .82); box-shadow: var(--shadow); overflow: hidden; }
.manual-panel summary {
padding: 1rem 1.1rem; color: var(--cyan); cursor: pointer; font-size: .78rem;
letter-spacing: .1em; text-transform: uppercase; list-style-position: inside;
}
.manual-panel summary:hover { background: rgba(30, 70, 89, .32); }
.manual-content { padding: 0 1rem 1.25rem; }
.manual-content p, .manual-content li { color: var(--muted); font-size: .75rem; line-height: 1.62; }
.manual-content h2 { margin: 1.1rem 0 .35rem; color: var(--ink); font-size: 1.05rem; }
.manual-content h3 { margin: 1.3rem 0 .25rem; color: var(--cyan); font-size: .82rem; letter-spacing: .08em; text-transform: uppercase; }
.manual-content strong { color: var(--ink); }
.manual-content ul, .manual-content ol { padding-left: 1.3rem; }
.manual-content li { margin: .3rem 0; }
.manual-content a { color: var(--cyan); }
.manual-note { padding: .7rem .8rem; border-left: 3px solid var(--yellow); background: rgba(255, 209, 102, .06); }
.manual-figures { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .8rem; margin: 1rem 0; }
.manual-figure { margin: 0; padding: .65rem; border: 1px solid var(--line); border-radius: 8px; background: #071823; }
.manual-figure svg { display: block; width: 100%; height: auto; }
.manual-figure figcaption { margin-top: .5rem; color: var(--muted); font-size: .68rem; line-height: 1.45; }
.manual-figure figcaption strong { color: var(--yellow); }
@media (max-width: 650px) { .manual-figures { grid-template-columns: 1fr; } }
.hidden { display: none !important; }
@media (max-width: 850px) {
.dashboard { grid-template-columns: 1fr; }
.dashboard .side { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: start; }
.dashboard .side .section-gap { margin-top: 0; }
}
@media (max-width: 560px) {
.shell { width: min(100% - 1rem, 1180px); padding-top: 1rem; }
header { display: block; } header .subtitle { margin-top: .8rem; }
form .field-grid, .dashboard .side { grid-template-columns: 1fr; }
.readout-value { font-size: 1.05rem; }
.control-grid { grid-template-columns: repeat(2, 1fr); }
.control-grid .wide { grid-column: span 2; }
}
</style>
</head>
<body>
<main class="shell">
<header>
<div>
<div class="kicker">Milton-Bradley electronic game simulation</div>
<h1>CODE NAME: SECTOR</h1>
</div>
<div class="subtitle">Hunt the hidden submarine using range, deduction, and a very limited number of bad guesses.</div>
</header>
<section class="screen">
<div class="screen-bar">
<span>Sector control console · browser edition</span>
<span id="consoleLamp" class="lamp off">standby</span>
</div>
<div id="setupView" class="setup">
<h2>Initialize mission</h2>
<p>This version keeps the submarine hidden and leaves the charting to you. Use the controls below in place of the original membrane keyboard. Coordinates begin at 25, 25; latitude increases northward and longitude increases eastward.</p>
<form id="setupForm">
<div class="field-grid">
<label>Map width (longitude)
<input id="mapWidth" type="number" min="20" max="74" value="50" required>
</label>
<label>Map height (latitude)
<input id="mapHeight" type="number" min="20" max="74" value="50" required>
</label>
</div>
<div class="check-row">
<label class="check"><input id="sound" type="checkbox" checked> Sound effects</label>
<label class="check"><input id="training" type="checkbox"> Training game</label>
<label class="check"><input id="evasive" type="checkbox"> Evasive submarine</label>
</div>
<div class="form-actions">
<button class="primary" type="submit">Start mission</button>
<span class="hint">Training places the sub at latitude 35, longitude 35, depth 1.</span>
</div>
</form>
</div>
<div id="gameView" class="game hidden">
<div class="dashboard">
<div class="main-column">
<div class="panel">
<div class="panel-title">Active vessel telemetry</div>
<div class="panel-body">
<div class="readout">
<div class="readout-cell"><span class="readout-label">ship</span><span id="shipReadout" class="readout-value">1</span></div>
<div class="readout-cell"><span class="readout-label">speed</span><span id="speedReadout" class="readout-value">0</span></div>
<div class="readout-cell"><span class="readout-label">latitude</span><span id="latReadout" class="readout-value">35</span></div>
<div class="readout-cell"><span class="readout-label">longitude</span><span id="lonReadout" class="readout-value">25</span></div>
</div>
<div id="status" class="status">READY<small>Obtain a range, then move or adjust your position.</small></div>
<div id="depthForm" class="depth-form">
<label>Fire depth (1–3)
<select id="depthSelect">
<option value="1">1 — shallow</option>
<option value="2">2 — middle</option>
<option value="3">3 — deep</option>
</select>
</label>
<button id="confirmFire" class="warn" type="button">Confirm fire</button>
</div>
</div>
</div>
<div class="panel section-gap">
<div class="panel-title">Direction / aim</div>
<div class="panel-body">
<div class="compass-wrap">
<div class="compass" aria-label="Compass">
<span class="dir n">N</span><span class="dir ne">NE</span><span class="dir e">E</span>
<span class="dir se">SE</span><span class="dir s">S</span><span class="dir sw">SW</span>
<span class="dir w">W</span><span class="dir nw">NW</span>
<div id="needle" class="needle"></div>
</div>
</div>
<div id="compassCaption" class="compass-caption">Course: NE · Aim: NE</div>
</div>
</div>
<div class="panel section-gap">
<div class="panel-title">Sector chart · red line marks the submarine's limit</div>
<div class="board-wrap">
<canvas id="boardCanvas" width="1000" height="580" aria-label="Graphical sector chart"></canvas>
<div class="board-legend">
<span class="legend-item"><i class="legend-swatch ship-red"></i> ship 1</span>
<span class="legend-item"><i class="legend-swatch ship-green"></i> ship 2</span>
<span class="legend-item"><i class="legend-swatch ship-blue"></i> ship 3</span>
<span class="legend-item"><i class="legend-swatch ship-yellow"></i> ship 4</span>
<span class="legend-item"><i class="legend-swatch range"></i> latest range box</span>
<span class="legend-item"><i class="legend-swatch limit"></i> submarine limit line</span>
<span class="legend-item"><i class="legend-swatch hidden-contact"></i> hidden contact</span>
</div>
</div>
</div>
<div class="panel section-gap">
<div class="panel-title">Mission controls</div>
<div class="panel-body">
<div class="control-grid">
<button id="nextBtn" class="primary wide" type="button">Next ship</button>
<button id="moveBtn" type="button">Move ship</button>
<button id="rangeBtn" type="button">Range</button>
<button id="leftBtn" type="button">← Turn / aim left</button>
<button id="rightBtn" type="button">Turn / aim right →</button>
<button id="slowerBtn" type="button">Slower</button>
<button id="fasterBtn" type="button">Faster</button>
<button id="aimBtn" type="button">Aim</button>
<button id="fireBtn" class="warn" type="button">Fire</button>
<button id="recallBtn" type="button">Recall</button>
<button id="giveUpBtn" class="danger" type="button">Give up</button>
</div>
</div>
</div>
</div>
<aside class="side">
<div class="panel">
<div class="panel-title">Fleet status</div>
<div class="panel-body"><div id="fleet" class="fleet"></div></div>
</div>
<div class="panel section-gap">
<div class="panel-title">Mission log</div>
<div id="log" class="log"></div>
</div>
<div class="panel section-gap">
<div class="panel-title">Field notes</div>
<div class="panel-body rules">
<strong>Range:</strong> distance is the larger of the latitude and longitude differences.<br>
<strong>Red limit line:</strong> the submarine may travel only inside the boundary five units from the map edge.<br>
<strong>F OK:</strong> range 0–1, or range 2 on a straight/45° line.<br>
<strong>OFF:</strong> correct shot line, wrong depth. The number is depth difference.<br>
<strong>SOS:</strong> wrong line or range; the submarine retaliates and relocates your ship.
</div>
</div>
</aside>
</div>
<p class="footer-note">The chart remains intentionally manual: mark each range box on graph paper or your own notes. The submarine moves one unit at each “Next ship” and keeps its depth.</p>
</div>
</section>
<section class="manual-panel">
<details>
<summary>Reference manual · Code Name: Sector</summary>
<div class="manual-content">
<h2>The challenge</h2>
<p>You are the commander of a destroyer. Your mission is to seek and destroy an enemy submarine. You sail into enemy waters with a fleet of three other ships. To locate the sub, share information with your fellow commanders, but only one commander can destroy it. Each commander wants the glory, so sink the sub before an allied commander does.</p>
<p>The computer is a double agent: it helps you and your allies find the sub, but it also controls the submarine's movement.</p>
<h2>Combat preparation</h2>
<ul>
<li>Set up four ships, numbered 1 through 4. Ship 1 moves first, followed by Ships 2, 3, and 4; repeat this order throughout the game.</li>
<li>Each player should keep a separate chart. The manual suggests different crayon colors: Ship 1 red, Ship 2 green, Ship 3 blue, and Ship 4 black.</li>
<li>Mark the lower-left chart corner as <strong>25N, 25E</strong>. Latitude increases upward; longitude increases to the right.</li>
<li>The sub is placed by the computer. Its exact location, course, and depth are hidden. Its depth is 1, 2, or 3.</li>
</ul>
<h2>Plotting the chart</h2>
<p>Press <strong>RANGE</strong> to learn the sub's distance from your current position. Range is the larger of the absolute latitude and longitude differences. For example, a difference of 4 latitude and 7 longitude produces a range of 7.</p>
<p>Plot the range as a complete square: count the range distance horizontally, vertically, and diagonally from the ship, then connect those points. Every point on that square is the same distance from the ship. The sub must be somewhere on the range line, unless later information eliminates that point.</p>
<p>When a later range is known, keep only the points on the earlier range line that are also the new distance from the new ship position. The shortest possible route along the range line is the useful route to count.</p>
<div class="manual-figures">
<figure class="manual-figure">
<svg viewBox="0 0 420 250" role="img" aria-label="Range square diagram">
<defs>
<pattern id="rangeGrid" width="18" height="18" patternUnits="userSpaceOnUse">
<path d="M 18 0 L 0 0 0 18" fill="none" stroke="#285367" stroke-width="1"/>
</pattern>
</defs>
<rect x="28" y="18" width="350" height="190" fill="url(#rangeGrid)" stroke="#4c8ba1"/>
<rect x="82" y="36" width="242" height="154" fill="none" stroke="#ff667a" stroke-width="3" stroke-dasharray="9 6"/>
<rect x="154" y="72" width="98" height="98" fill="rgba(255,209,102,.08)" stroke="#ffd166" stroke-width="3"/>
<circle cx="203" cy="121" r="7" fill="#45e0e8"/>
<text x="211" y="117" fill="#eaf5ff" font-size="12" font-family="monospace">YOUR SHIP</text>
<text x="163" y="66" fill="#ffd166" font-size="12" font-family="monospace">RANGE 5</text>
<text x="88" y="31" fill="#ff667a" font-size="11" font-family="monospace">LIMIT LINE</text>
<text x="25" y="229" fill="#91a8bb" font-size="11" font-family="monospace">25E</text>
<text x="345" y="229" fill="#91a8bb" font-size="11" font-family="monospace">50E</text>
<text x="3" y="26" fill="#91a8bb" font-size="11" font-family="monospace">50N</text>
<text x="3" y="207" fill="#91a8bb" font-size="11" font-family="monospace">25N</text>
<text x="28" y="13" fill="#65e5a0" font-size="11" font-family="monospace">LAT ↑</text>
<text x="316" y="245" fill="#91a8bb" font-size="11" font-family="monospace">LON →</text>
</svg>
<figcaption><strong>Range square:</strong> the submarine is somewhere on the square at the reported distance. The red limit line is five units inside the chart edge.</figcaption>
</figure>
<figure class="manual-figure">
<svg viewBox="0 0 420 250" role="img" aria-label="Firing directions diagram">
<defs>
<pattern id="fireGrid" width="26" height="26" patternUnits="userSpaceOnUse">
<path d="M 26 0 L 0 0 0 26" fill="none" stroke="#285367" stroke-width="1"/>
</pattern>
</defs>
<rect x="42" y="18" width="208" height="208" fill="url(#fireGrid)" stroke="#4c8ba1"/>
<line x1="146" y1="18" x2="146" y2="226" stroke="#4c8ba1"/>
<line x1="42" y1="122" x2="250" y2="122" stroke="#4c8ba1"/>
<circle cx="146" cy="122" r="9" fill="#45e0e8"/>
<text x="158" y="117" fill="#eaf5ff" font-size="11" font-family="monospace">SHIP</text>
<g fill="#65e5a0">
<circle cx="146" cy="96" r="6"/><circle cx="146" cy="70" r="6"/>
<circle cx="172" cy="96" r="6"/><circle cx="198" cy="70" r="6"/>
<circle cx="172" cy="122" r="6"/><circle cx="198" cy="122" r="6"/>
<circle cx="172" cy="148" r="6"/><circle cx="198" cy="174" r="6"/>
<circle cx="146" cy="148" r="6"/><circle cx="146" cy="174" r="6"/>
<circle cx="120" cy="148" r="6"/><circle cx="94" cy="174" r="6"/>
<circle cx="120" cy="122" r="6"/><circle cx="94" cy="122" r="6"/>
<circle cx="120" cy="96" r="6"/><circle cx="94" cy="70" r="6"/>
</g>
<g fill="#ff667a">
<circle cx="198" cy="96" r="5"/><circle cx="198" cy="148" r="5"/>
<circle cx="94" cy="96" r="5"/><circle cx="94" cy="148" r="5"/>
</g>
<text x="282" y="78" fill="#65e5a0" font-size="12" font-family="monospace">GREEN = fire</text>
<text x="282" y="98" fill="#65e5a0" font-size="12" font-family="monospace">8 compass</text>
<text x="282" y="118" fill="#65e5a0" font-size="12" font-family="monospace">directions</text>
<text x="282" y="157" fill="#ff667a" font-size="12" font-family="monospace">RED = not fire</text>
<text x="282" y="177" fill="#ff667a" font-size="12" font-family="monospace">range 2 only</text>
</svg>
<figcaption><strong>Firing pattern:</strong> a shot may travel up to range 2 in any of the eight compass directions. Other positions at range 2 are not valid firing positions.</figcaption>
</figure>
</div>
<h2>Moving a ship</h2>
<ol>
<li>Choose a compass direction: N, NE, E, SE, S, SW, W, or NW.</li>
<li>Set a speed from 0 through 9. A ship moves that many points when MOVE SHIP is pressed.</li>
<li>Plot the new position and course on your chart. Plotting is only a record; it does not affect the ship's movement.</li>
<li>Ships may not occupy the same point. A collision knocks the moving ship to a new random location and course; its speed remains as it was.</li>
</ol>
<p>Press <strong>NEXT SHIP</strong> to pass control. Each time NEXT SHIP is pressed, the submarine moves exactly one point, even if the active ship did not move. The submarine keeps its depth and normally keeps its course.</p>
<h2>Firing restrictions</h2>
<p>A ship can fire in any one of the eight compass directions, up to a range of 2 points. At a distance of 1 or directly overhead, the computer always displays an <strong>F</strong> before the range number. At a distance of 2, it displays F only when the sub lies in one of the eight valid firing directions.</p>
<p>If the display does not show F and you fire anyway, the shot misses and produces <strong>SOS</strong>. At range 2, being diagonally or horizontally/vertically misaligned matters: only the eight compass points at that distance are fireable.</p>
<h2>Aiming and the result</h2>
<ul>
<li>Press <strong>AIM</strong>, then select the firing direction. In the original game, AIM cycles the compass; this HTML version uses the left/right aim controls.</li>
<li>Choose a firing depth of 1, 2, or 3. The submarine's depth does not change during the game.</li>
<li><strong>SUB</strong> means a direct hit and ends the game.</li>
<li><strong>OFF 1</strong> or <strong>OFF 2</strong> means the direction and range were correct but the depth guess was off by one or two levels.</li>
<li><strong>SOS</strong> means the direction or range was wrong. The submarine retaliates by knocking the ship to a new random position and course. Use RECALL to read the new information, then plot it.</li>
</ul>
<div class="manual-figures">
<figure class="manual-figure">
<svg viewBox="0 0 420 220" role="img" aria-label="Submarine limit line behavior diagram">
<defs>
<pattern id="limitGrid" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#285367" stroke-width="1"/>
</pattern>
</defs>
<rect x="34" y="18" width="260" height="178" fill="url(#limitGrid)" stroke="#4c8ba1"/>
<rect x="84" y="48" width="160" height="118" fill="none" stroke="#ff667a" stroke-width="3" stroke-dasharray="8 5"/>
<circle cx="84" cy="107" r="7" fill="#ff9d52"/>
<text x="96" y="103" fill="#eaf5ff" font-size="11" font-family="monospace">SUB ON LIMIT</text>
<path d="M84 107 L116 75 M84 107 L125 107 M84 107 L116 139" fill="none" stroke="#ffd166" stroke-width="3"/>
<path d="M116 75 l-10 2 7 7 M125 107 l-10 -5 v10 M116 139 l-7 -9 -4 9" fill="#ffd166"/>
<text x="313" y="78" fill="#ffd166" font-size="11" font-family="monospace">3 possible</text>
<text x="313" y="96" fill="#ffd166" font-size="11" font-family="monospace">new courses</text>
<text x="313" y="130" fill="#ff667a" font-size="11" font-family="monospace">red limit</text>
<text x="313" y="148" fill="#ff667a" font-size="11" font-family="monospace">line</text>
<text x="36" y="211" fill="#91a8bb" font-size="11" font-family="monospace">sub turns inward at NEXT SHIP</text>
</svg>
<figcaption><strong>Limit line:</strong> if the submarine reaches the red line and remains there, its next move turns it back toward the combat area. Its new course is one of three directions related to that edge.</figcaption>
</figure>
</div>
<h2>Evasive submarine</h2>
<p>EVASIVE SUB is an optional advanced mode. After each unsuccessful firing—OFF 1, OFF 2, or SOS—the submarine may turn left 45°, turn right 45°, or maintain its current course. This option changes only the sub's course, not its depth or speed.</p>
<h2>Single-person play and scoring</h2>
<p>A solo player may command the entire fleet or choose only two or three ships. In the two-ship example, the player can bypass the other ships by pressing NEXT SHIP until the desired ship is active.</p>
<p>Optional scoring: <strong>SUB</strong> is worth 10 points, <strong>OFF 1</strong> is worth 5 points, and <strong>OFF 2</strong> is worth 3 points. The winning score is up to the players; the manual suggests deciding it before play.</p>
<h2>Quick turn checklist</h2>
<ol>
<li>Press RANGE and plot the range information.</li>
<li>Choose course and speed, then MOVE SHIP and plot the new position.</li>
<li>Press RANGE again and narrow the possible submarine positions.</li>
<li>If F appears, AIM in a valid direction and choose depth before FIRE.</li>
<li>Press NEXT SHIP. The submarine moves one point.</li>
</ol>
<p class="manual-note"><strong>Browser controls:</strong> this recreation combines AIM plus the firing-depth sequence into a web form. RECALL exits aim mode and displays the vessel's current position and course; GIVE UP reveals the submarine and ends the mission, corresponding to the manual's SUB FINDER / reveal behavior.</p>
</div>
</details>
</section>
</main>
<script>
"use strict";
const DIRECTIONS = [
{ name: "N", lat: 1, lon: 0, angle: 0 },
{ name: "NE", lat: 1, lon: 1, angle: 45 },
{ name: "E", lat: 0, lon: 1, angle: 90 },
{ name: "SE", lat: -1, lon: 1, angle: 135 },
{ name: "S", lat: -1, lon: 0, angle: 180 },
{ name: "SW", lat: -1, lon: -1, angle: 225 },
{ name: "W", lat: 0, lon: -1, angle: 270 },
{ name: "NW", lat: 1, lon: -1, angle: 315 }
];
const SHIP_COLORS = ["#ff667a", "#65e5a0", "#5aa9ff", "#ffd166"];
const state = {
width: 50, height: 50, sound: true, evasive: false, training: false,
ships: [], sub: null, ship: 0, moved: false, fired: false,
aiming: false, gameOver: false, range: null, statusTone: ""
};
const $ = (id) => document.getElementById(id);
const setupView = $("setupView");
const gameView = $("gameView");
const setupForm = $("setupForm");
const logEl = $("log");
const boardCanvas = $("boardCanvas");
const audio = window.AudioContext || window.webkitAudioContext;
let audioContext = null;
function direction(index) { return DIRECTIONS[(index + 8) % 8]; }
function randomInt(max) { return Math.floor(Math.random() * max); }
function directionIndex() { return randomInt(8); }
function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); }
function beep(kind) {
if (!state.sound || !audio) return;
try {
audioContext = audioContext || new audio();
const now = audioContext.currentTime;
const oscillator = audioContext.createOscillator();
const gain = audioContext.createGain();
oscillator.connect(gain); gain.connect(audioContext.destination);
const tones = { ping: [720, .08], fire: [180, .16], hit: [90, .55], bad: [420, .3], collide: [120, .22] };
const [frequency, duration] = tones[kind] || tones.ping;
oscillator.frequency.setValueAtTime(frequency, now);
if (kind === "ping") oscillator.frequency.exponentialRampToValueAtTime(1040, now + duration);
if (kind === "bad") oscillator.frequency.exponentialRampToValueAtTime(180, now + duration);
gain.gain.setValueAtTime(.0001, now);
gain.gain.exponentialRampToValueAtTime(.12, now + .01);
gain.gain.exponentialRampToValueAtTime(.0001, now + duration);
oscillator.start(now); oscillator.stop(now + duration + .02);
} catch (_) {}
}
function addLog(message, type = "") {
const entry = document.createElement("div");
entry.className = type;
entry.textContent = message;
logEl.prepend(entry);
}
function setStatus(label, detail = "", tone = "") {
const status = $("status");
status.className = `status ${tone}`;
status.textContent = label;
if (detail) {
const small = document.createElement("small");
small.textContent = detail;
status.appendChild(small);
}
state.statusTone = tone;
}
function setupMission(event) {
event.preventDefault();
const width = Number($("mapWidth").value);
const height = Number($("mapHeight").value);
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 20 || height < 20 || width > 74 || height > 74) {
setStatus("INVALID MAP", "Width and height must be whole numbers from 20 to 74.", "bad");
return;
}
state.width = width; state.height = height;
state.sound = $("sound").checked; state.training = $("training").checked; state.evasive = $("evasive").checked;
state.ships = [
{ lat: 35, lon: 25, speed: 0, course: 1, aim: 1 },
{ lat: 30, lon: 25, speed: 0, course: 1, aim: 1 },
{ lat: 25, lon: 30, speed: 0, course: 1, aim: 1 },
{ lat: 25, lon: 35, speed: 0, course: 1, aim: 1 }
];
state.ship = 0; state.moved = false; state.fired = false; state.aiming = false;
state.gameOver = false; state.range = null;
state.sub = state.training
? { lat: 35, lon: 35, course: 1, depth: 1 }
: {
lat: 30 + randomInt(height - 10),
lon: 30 + randomInt(width - 10),
course: directionIndex(),
depth: 1 + randomInt(3)
};
if (state.training) state.evasive = false;
setupView.classList.add("hidden"); gameView.classList.remove("hidden");
$("consoleLamp").className = "lamp";
$("consoleLamp").textContent = state.evasive ? "evasive contact" : "mission active";
logEl.textContent = "";
addLog(`Mission started on a ${width} × ${height} sector.`);
addLog(state.training ? "Training coordinates loaded." : "Submarine contact concealed.", "important");
if (state.evasive) addLog("Evasive behavior enabled: missed shots may turn the sub.", "important");
render();
}
function render() {
const ship = state.ships[state.ship];
$("shipReadout").textContent = state.ship + 1;
$("speedReadout").textContent = ship.speed;
$("latReadout").textContent = ship.lat;
$("lonReadout").textContent = ship.lon;
const aim = direction(state.aiming ? ship.aim : ship.course);
$("needle").style.transform = `translate(-50%, -100%) rotate(${aim.angle}deg)`;
$("compassCaption").textContent = `Course: ${direction(ship.course).name} · Aim: ${direction(ship.aim).name}${state.aiming ? " · AIMING" : ""}`;
renderFleet();
drawBoard();
const buttons = ["nextBtn", "moveBtn", "rangeBtn", "leftBtn", "rightBtn", "slowerBtn", "fasterBtn", "aimBtn", "fireBtn", "recallBtn", "giveUpBtn"];
buttons.forEach(id => $(id).disabled = state.gameOver);
$("moveBtn").disabled = state.gameOver || state.moved;
$("nextBtn").disabled = state.gameOver;
$("depthForm").classList.toggle("visible", state.aiming && state.fired && !state.gameOver);
$("fireBtn").disabled = state.gameOver || state.fired;
$("aimBtn").disabled = state.gameOver || state.fired;
$("leftBtn").disabled = state.gameOver;
$("rightBtn").disabled = state.gameOver;
}
function renderFleet() {
$("fleet").innerHTML = state.ships.map((ship, index) => `
<div class="ship-card ${index === state.ship ? "active" : ""}">
<div class="ship-badge">${index + 1}</div>
<div>
<div class="ship-name">Ship ${index + 1}</div>
<div class="ship-meta">${ship.lat} N · ${ship.lon} E · ${direction(ship.course).name} · speed ${ship.speed}</div>
</div>
${index === state.ship && !state.gameOver ? '<div class="turn-tag">ACTIVE</div>' : ""}
</div>
`).join("");
}
function drawBoard() {
if (!state.sub || !boardCanvas) return;
const ctx = boardCanvas.getContext("2d");
const cssWidth = Math.max(320, boardCanvas.clientWidth || 900);
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const cssHeight = Math.max(260, Math.round(cssWidth * .58));
boardCanvas.width = Math.round(cssWidth * dpr);
boardCanvas.height = Math.round(cssHeight * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, cssWidth, cssHeight);
const margin = { left: 42, right: 14, top: 17, bottom: 31 };
const cell = Math.min(
(cssWidth - margin.left - margin.right) / state.width,
(cssHeight - margin.top - margin.bottom) / state.height
);
const chartWidth = state.width * cell;
const chartHeight = state.height * cell;
const left = margin.left + ((cssWidth - margin.left - margin.right) - chartWidth) / 2;
const top = margin.top + ((cssHeight - margin.top - margin.bottom) - chartHeight) / 2;
const maxLat = 25 + state.height;
const x = (lon) => left + (lon - 25) * cell;
const y = (lat) => top + (maxLat - lat) * cell;
ctx.fillStyle = "#071823";
ctx.fillRect(0, 0, cssWidth, cssHeight);
ctx.strokeStyle = "rgba(73, 148, 170, .29)";
ctx.lineWidth = 1;
for (let i = 0; i <= state.width; i++) {
const px = x(25 + i) + .5;
ctx.beginPath(); ctx.moveTo(px, top); ctx.lineTo(px, top + chartHeight); ctx.stroke();
}
for (let i = 0; i <= state.height; i++) {
const py = y(25 + i) + .5;
ctx.beginPath(); ctx.moveTo(left, py); ctx.lineTo(left + chartWidth, py); ctx.stroke();
}
ctx.strokeStyle = "#4c8ba1";
ctx.strokeRect(left + .5, top + .5, chartWidth, chartHeight);
ctx.fillStyle = "#91a8bb";
ctx.font = "10px ui-monospace, monospace";
ctx.textAlign = "center";
const lonStep = state.width > 55 ? 10 : 5;
for (let lon = 25; lon <= 25 + state.width; lon += lonStep) {
ctx.fillText(String(lon), x(lon), top + chartHeight + 17);
}
ctx.textAlign = "right";
const latStep = state.height > 55 ? 10 : 5;
for (let lat = 25; lat <= 25 + state.height; lat += latStep) {
ctx.fillText(String(lat), left - 7, y(lat) + 3);
}
ctx.textAlign = "left";
ctx.fillStyle = "#65e5a0";
ctx.fillText("LAT ↑", 5, top + 4);
ctx.fillStyle = "#91a8bb";
ctx.fillText("LON →", left + chartWidth - 34, cssHeight - 7);
const limitLeft = x(30);
const limitTop = y(20 + state.height);
const limitWidth = (state.width - 10) * cell;
const limitHeight = (state.height - 10) * cell;
ctx.save();
ctx.setLineDash([7, 5]);
ctx.strokeStyle = "#ff667a";
ctx.lineWidth = 2;
ctx.strokeRect(limitLeft + .5, limitTop + .5, limitWidth, limitHeight);
ctx.restore();
ctx.fillStyle = "#ff667a";
ctx.font = "bold 10px ui-monospace, monospace";
ctx.fillText("SUB LIMIT", limitLeft + 5, limitTop + 12);
const activeShip = state.ships[state.ship];
if (state.range !== null) {
const radius = Math.max(1, state.range);
const boxLeft = x(activeShip.lon - radius);
const boxTop = y(activeShip.lat + radius);
const boxWidth = radius * 2 * cell;
ctx.save();
ctx.setLineDash([5, 4]);
ctx.strokeStyle = "#ffd166";
ctx.fillStyle = "rgba(255, 209, 102, .06)";
ctx.lineWidth = 2;
ctx.fillRect(boxLeft, boxTop, boxWidth, boxWidth);
ctx.strokeRect(boxLeft, boxTop, boxWidth, boxWidth);
ctx.restore();
ctx.fillStyle = "#ffd166";
ctx.font = "bold 10px ui-monospace, monospace";
ctx.fillText(`R${state.range}`, boxLeft + 4, Math.max(top + 11, boxTop + 11));
}
state.ships.forEach((ship, index) => {
const px = x(ship.lon), py = y(ship.lat);
const shipColor = SHIP_COLORS[index];
ctx.save();
ctx.translate(px, py);
if (index === state.ship) {
ctx.rotate(Math.PI / 4);
ctx.fillStyle = shipColor;
ctx.fillRect(-cell * .36, -cell * .36, cell * .72, cell * .72);
ctx.rotate(-Math.PI / 4);
ctx.strokeStyle = "#eaf5ff";
ctx.lineWidth = 1.5;
ctx.strokeRect(-cell * .42, -cell * .42, cell * .84, cell * .84);
} else {
ctx.fillStyle = shipColor;
ctx.beginPath(); ctx.arc(0, 0, Math.max(3, cell * .28), 0, Math.PI * 2); ctx.fill();
}
ctx.fillStyle = "#062029";
ctx.font = "bold 10px ui-monospace, monospace";
ctx.textAlign = "center"; ctx.textBaseline = "middle";
ctx.fillText(String(index + 1), 0, 0);
ctx.restore();
});
ctx.fillStyle = "#ff9d52";
ctx.font = "10px ui-monospace, monospace";
ctx.fillText("CONTACT NOT DISPLAYED", left + 8, top + chartHeight - 8);
}
function nextShip() {
if (state.gameOver) return;
moveSub();
state.ship = (state.ship + 1) % state.ships.length;
state.moved = false; state.fired = false; state.aiming = false; state.range = null;
setStatus("READY", `Ship ${state.ship + 1} has the conn. Range before you fire.`, "");
addLog(`Next ship: vessel ${state.ship + 1} is active.`);
render();
}
function moveSub() {
let subDirection = direction(state.sub.course);
const nextLat = state.sub.lat + subDirection.lat;
const nextLon = state.sub.lon + subDirection.lon;
if (nextLat < 30 || nextLat > state.height + 20 || nextLon < 30 || nextLon > state.width + 20) {
const requiredLat = nextLat < 30 ? 1 : (nextLat > state.height + 20 ? -1 : 0);
const requiredLon = nextLon < 30 ? 1 : (nextLon > state.width + 20 ? -1 : 0);
let choices = DIRECTIONS.map((_, index) => index).filter(index => {
const d = direction(index);
const lat = state.sub.lat + d.lat, lon = state.sub.lon + d.lon;
const latIsValid = lat >= 30 && lat <= state.height + 20;
const lonIsValid = lon >= 30 && lon <= state.width + 20;
const headsBack = (!requiredLat || d.lat === requiredLat) && (!requiredLon || d.lon === requiredLon);
return latIsValid && lonIsValid && headsBack;
});
// On a straight edge, this produces the three courses that point
// inward from the limit line, matching the printed rules.
if (!choices.length) {
choices = DIRECTIONS.map((_, index) => index).filter(index => {
const d = direction(index);
return state.sub.lat + d.lat >= 30 && state.sub.lat + d.lat <= state.height + 20 &&
state.sub.lon + d.lon >= 30 && state.sub.lon + d.lon <= state.width + 20;
});
}
state.sub.course = choices[randomInt(choices.length)];
subDirection = direction(state.sub.course);
}
state.sub.lat += subDirection.lat;
state.sub.lon += subDirection.lon;
}
function moveShip() {
if (state.gameOver || state.moved) {
if (state.moved) setStatus("NEXT", "This ship has already moved. Press Next ship.", "warn");
return;
}
state.moved = true;
const ship = state.ships[state.ship];
let collided = false;
for (let step = 0; step < ship.speed; step++) {
ship.lat += direction(ship.course).lat; ship.lon += direction(ship.course).lon;
if (ship.lat < 25 || ship.lat > 25 + state.height || ship.lon < 25 || ship.lon > 25 + state.width) {
ship.lat = clamp(ship.lat, 25, 25 + state.height);
ship.lon = clamp(ship.lon, 25, 25 + state.width);
ship.speed = 0;
}
if (state.ships.some((other, index) => index !== state.ship && other.lat === ship.lat && other.lon === ship.lon)) {
collided = true; break;
}
}
if (collided) {
ship.lat = 25 + randomInt(state.width + 1);
ship.lon = 25 + randomInt(state.height + 1);
ship.course = directionIndex(); ship.aim = ship.course; ship.speed = 0;
setStatus("COLLISION", "Vessel relocated. Press Recall to read the new course.", "bad");
addLog(`Ship ${state.ship + 1} collided and was relocated.`, "failure");
beep("collide");
} else {
setStatus("MOVE", "Course updated. The submarine stays still until Next ship.", "");
addLog(`Ship ${state.ship + 1} moved at speed ${ship.speed}.`);
}
render();
}
function turn(delta) {
if (state.gameOver) return;
const ship = state.ships[state.ship];
const key = state.aiming ? "aim" : "course";
ship[key] = (ship[key] + delta + 8) % 8;
setStatus(state.aiming ? "AIM" : "COURSE", `${state.aiming ? "Aim" : "Course"} set to ${direction(ship[key]).name}.`, "");
render();
}
function changeSpeed(delta) {
if (state.gameOver) return;
const ship = state.ships[state.ship];
ship.speed = clamp(ship.speed + delta, 0, 9);
setStatus("SPEED", `Speed set to ${ship.speed}.`, "");
render();
}
function rangeCheck() {
if (state.gameOver) return;
const ship = state.ships[state.ship];
const r1 = Math.abs(state.sub.lat - ship.lat);
const r2 = Math.abs(state.sub.lon - ship.lon);
state.range = Math.max(r1, r2);
beep("ping");
const fOk = state.range <= 1 || (state.range === 2 && (r1 === 0 || r2 === 0 || r1 === r2));
if (fOk) {
setStatus("F OK", `Range ${state.range}. A clear shot is possible.`, "good");
addLog(`Range: ${state.range} · F OK`, "success");
} else {
setStatus(`RANGE ${state.range}`, "No clear shot at this position.", "");
addLog(`Range: ${state.range} · no clear shot.`);
}
render();
}
function beginAim() {
if (state.gameOver || state.fired) return;
state.aiming = true;
const ship = state.ships[state.ship];
setStatus("AIM", "Turn the aim, then press Fire. Recall exits aim mode.", "warn");
addLog(`Aiming at ${direction(ship.aim).name}.`, "important");
render();
}
function beginFire() {
if (state.gameOver || state.fired) return;
if (!state.aiming) beginAim();
if (!state.aiming) return;
state.fired = true;
setStatus("ENTER DEPTH", "Choose 1, 2, or 3, then confirm fire.", "warn");
render();
}
function confirmFire() {
if (state.gameOver || !state.aiming || !state.fired) return;
const depth = Number($("depthSelect").value);
const ship = state.ships[state.ship];
const r1 = Math.abs(state.sub.lat - ship.lat);
const r2 = Math.abs(state.sub.lon - ship.lon);
const range = Math.max(r1, r2);
const fOk = range <= 1 || (range === 2 && (r1 === 0 || r2 === 0 || r1 === r2));
const aim = direction(ship.aim);
const direct = range === 0 ||
(Math.sign(state.sub.lat - ship.lat) === aim.lat && Math.sign(state.sub.lon - ship.lon) === aim.lon);
beep("fire");
$("depthForm").classList.remove("visible");
if (fOk && direct && depth === state.sub.depth) {
setStatus("SUB!", `Direct hit at depth ${depth}. Mission complete.`, "good");
addLog(`HIT · submarine found at ${state.sub.lat} N, ${state.sub.lon} E, depth ${state.sub.depth}.`, "success");
state.gameOver = true; state.aiming = false; beep("hit");
$("consoleLamp").className = "lamp"; $("consoleLamp").textContent = "contact destroyed";
} else if (fOk && direct) {
const off = Math.abs(depth - state.sub.depth);
setStatus("OFF", `Correct direction, wrong depth · ${off} level${off === 1 ? "" : "s"} off.`, "warn");
addLog(`OFF · depth was ${off} level${off === 1 ? "" : "s"} away.`, "important");
state.aiming = false; beep("bad"); evasiveTurn();
} else {
setStatus("SOS!", "The submarine retaliated. Ship position and course changed.", "bad");
addLog("SOS · shot missed; vessel relocated.", "failure");
retaliate();
state.aiming = false; beep("bad");
}
render();
}
function evasiveTurn() {
if (!state.evasive) return;
const outcome = randomInt(3);
if (outcome === 0) {
state.sub.course = (state.sub.course - 1 + 8) % 8;
addLog(`Evasive sub turned left to ${direction(state.sub.course).name}.`, "important");
} else if (outcome === 1) {
state.sub.course = (state.sub.course + 1) % 8;
addLog(`Evasive sub turned right to ${direction(state.sub.course).name}.`, "important");
} else {
addLog("Evasive sub held its course.", "important");
}
}
function retaliate() {
const ship = state.ships[state.ship];
ship.lat = 25 + randomInt(state.width + 1);
ship.lon = 25 + randomInt(state.height + 1);
ship.course = directionIndex(); ship.aim = ship.course;
evasiveTurn();
}
function recall() {
if (state.gameOver) return;
state.aiming = false;
state.fired = false;
const ship = state.ships[state.ship];
setStatus("RECALL", `Ship ${state.ship + 1}: ${ship.lat} N, ${ship.lon} E · course ${direction(ship.course).name}.`, "");
render();
}
function giveUp() {
if (state.gameOver) return;
state.gameOver = true; state.aiming = false; state.fired = false;
setStatus("GIVE UP", `Submarine: ${state.sub.lat} N, ${state.sub.lon} E · ${direction(state.sub.course).name} · depth ${state.sub.depth}.`, "secret");
addLog(`Mission ended · submarine was at ${state.sub.lat} N, ${state.sub.lon} E, depth ${state.sub.depth}.`, "important");
$("consoleLamp").className = "lamp off"; $("consoleLamp").textContent = "mission ended";
render();
}
setupForm.addEventListener("submit", setupMission);
$("nextBtn").addEventListener("click", nextShip);
$("moveBtn").addEventListener("click", moveShip);
$("rangeBtn").addEventListener("click", rangeCheck);
$("leftBtn").addEventListener("click", () => turn(-1));
$("rightBtn").addEventListener("click", () => turn(1));
$("slowerBtn").addEventListener("click", () => changeSpeed(-1));
$("fasterBtn").addEventListener("click", () => changeSpeed(1));
$("aimBtn").addEventListener("click", beginAim);
$("fireBtn").addEventListener("click", beginFire);
$("confirmFire").addEventListener("click", confirmFire);
$("recallBtn").addEventListener("click", recall);
$("giveUpBtn").addEventListener("click", giveUp);
window.addEventListener("resize", () => { if (!gameView.classList.contains("hidden")) drawBoard(); });
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment