Batch of LLM-assisted fixes/feature updates
This commit is contained in:
1 parent
7c6453160a
commit
05aaf394df
16 files changed
+5597
-912
No files matched your search
+689
-336
File diff suppressed because it is too large.
Load diff
+181
-49
@@ -7,7 +7,6 @@
|
||||
<meta property="og:description" content="A collaborative story-writing game. Remake of the 2000s GameSpy Arcade classic." />
|
||||
<title>Y.A.R.N. HTML5</title>
|
||||
|
||||
<!-- re-use the same CSS-variables and base look -->
|
||||
<style>
|
||||
:root {
|
||||
--bg-main: #4b0082;
|
||||
@@ -18,12 +17,13 @@
|
||||
}
|
||||
* { margin:0; padding:0; box-sizing:border-box; font-family:system-ui, sans-serif; }
|
||||
body {
|
||||
height:100vh;
|
||||
min-height:100vh;
|
||||
background:var(--bg-main);
|
||||
color:var(--text-beige);
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
padding:1.5rem 0;
|
||||
}
|
||||
#splash {
|
||||
background:var(--box-bg);
|
||||
@@ -31,8 +31,11 @@
|
||||
border-radius:8px;
|
||||
padding:2rem 3rem;
|
||||
min-width:320px;
|
||||
max-width:560px;
|
||||
width:100%;
|
||||
text-align:center;
|
||||
}
|
||||
#splash > img { max-width:100%; }
|
||||
h1 { margin-bottom:1.5rem; letter-spacing:1px; }
|
||||
.section { margin:1.2rem 0; }
|
||||
input[type=text], button {
|
||||
@@ -67,50 +70,101 @@
|
||||
.room-row {
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
gap:.5rem;
|
||||
padding:.4rem .6rem;
|
||||
cursor:pointer;
|
||||
border-radius:3px;
|
||||
}
|
||||
.room-row:hover { background:#222; }
|
||||
.room-name { flex:1; }
|
||||
.room-count { color:var(--player-green); }
|
||||
.no-rooms { color:#888; font-style:italic; }
|
||||
|
||||
/* About overlay */
|
||||
#about-overlay {
|
||||
.room-name { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.room-count { color:var(--player-green); white-space:nowrap; }
|
||||
.room-status {
|
||||
font-size:.7rem;
|
||||
text-transform:uppercase;
|
||||
padding:.1rem .35rem;
|
||||
border-radius:3px;
|
||||
background:#3a0066;
|
||||
color:#e6d9ff;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.no-rooms, .hint { color:#888; font-style:italic; font-size:.85rem; }
|
||||
|
||||
/* Overlays */
|
||||
.overlay {
|
||||
position:fixed; inset:0;
|
||||
background:rgba(0,0,0,.75);
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
padding:1rem;
|
||||
z-index:10;
|
||||
}
|
||||
#about-box {
|
||||
.overlay-box {
|
||||
background:var(--box-bg);
|
||||
border:2px solid var(--accent-purple);
|
||||
border-radius:8px;
|
||||
padding:2rem 2.5rem;
|
||||
max-width:640px;
|
||||
max-width:720px;
|
||||
width:100%;
|
||||
max-height:88vh;
|
||||
overflow-y:auto;
|
||||
color:var(--text-beige);
|
||||
line-height:1.45;
|
||||
}
|
||||
#about-box h3 { margin-top:0; margin-bottom:1rem; }
|
||||
#about-box p { margin:.6rem 0; }
|
||||
#about-box ul { margin:.6rem 0 1rem 1.2rem; }
|
||||
#about-box button {
|
||||
.overlay-box h3 { margin-top:0; margin-bottom:1rem; }
|
||||
.overlay-box p { margin:.6rem 0; }
|
||||
.overlay-box ul { margin:.6rem 0 1rem 1.2rem; }
|
||||
.overlay-box button {
|
||||
margin-top:1rem; width:auto; padding:.6rem 1.2rem;
|
||||
}
|
||||
.overlay-actions { display:flex; gap:.6rem; flex-wrap:wrap; justify-content:center; }
|
||||
|
||||
#stories-list {
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:.35rem;
|
||||
max-height:45vh;
|
||||
overflow-y:auto;
|
||||
margin:1rem 0;
|
||||
}
|
||||
.story-row {
|
||||
display:flex; justify-content:space-between; align-items:center; gap:.5rem;
|
||||
padding:.45rem .6rem;
|
||||
background:#151515;
|
||||
border:1px solid #2a2a2a;
|
||||
border-radius:4px;
|
||||
cursor:pointer;
|
||||
font-size:.9rem;
|
||||
}
|
||||
.story-row:hover { border-color:var(--accent-purple); }
|
||||
.story-row .story-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
|
||||
#story-viewer {
|
||||
white-space:pre-wrap;
|
||||
font-family:"Times New Roman", Times, serif;
|
||||
font-size:1.05rem;
|
||||
line-height:1.5;
|
||||
background:#111;
|
||||
border:1px solid #2a2a2a;
|
||||
border-radius:4px;
|
||||
padding:1rem;
|
||||
max-height:50vh;
|
||||
overflow-y:auto;
|
||||
}
|
||||
|
||||
.hidden { display:none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="splash">
|
||||
<img src="images/yarnlogo.png" />
|
||||
<img src="images/yarnlogo.png" alt="Y.A.R.N. logo" />
|
||||
<h4 style="font-style: italic; color:orangered">HTML5 Edition</h4>
|
||||
|
||||
<!-- ENTER GAME -->
|
||||
<div class="section">
|
||||
<input id="room-input" type="text" placeholder="Room name..." maxlength="30"/>
|
||||
<button id="enter-btn">Enter Game</button>
|
||||
<div class="hint">Letters, numbers, dashes and underscores only.</div>
|
||||
</div>
|
||||
|
||||
<!-- FIND GAME -->
|
||||
@@ -119,76 +173,103 @@
|
||||
<div id="room-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- PAST STORIES -->
|
||||
<div class="section">
|
||||
<button id="stories-btn">Past Stories</button>
|
||||
</div>
|
||||
|
||||
<!-- ABOUT -->
|
||||
<div class="section">
|
||||
<button id="about-btn">About Y.A.R.N.</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ABOUT OVERLAY -->
|
||||
<div id="about-overlay" class="hidden">
|
||||
<div id="about-box">
|
||||
<h3>What is Y.A.R.N. HTML5?</h3>
|
||||
<p>Y.A.R.N. (Yet Another Random Narrative) was a collaborative story-writing "parlor game" originally included in GameSpy Arcade in the early 2000s, but removed shortly thereafter due to security issues that were never resolved.</p>
|
||||
<p><strong>Y.A.R.N. HTML5</strong> is a modern remake of (what I can remember from) the original, designed to run in-browser, and with extra features such as (optional) LLM-based bots.</p>
|
||||
<p style="text-align: left;"><strong>The rules are simple:</strong></p>
|
||||
<ul style="text-align: left;">
|
||||
<li>Players have a limited amount of time to write a sentence, selected by the host at game start.</li>
|
||||
<li>Once all players have submitted their sentences, players have 30 seconds to vote for their favorite.</li>
|
||||
<li>All players receive one point for each vote their sentence receives. The player whose sentence receives the most votes is awarded 3 bonus points.</li>
|
||||
<li>In the case of a tie, the longest sentence receives two extra points and the other sentences tied receive 1 point each.</li>
|
||||
<li>When a player reaches the point limit, they win the game.</li>
|
||||
</ul>
|
||||
<p>Enjoy the story-writing!<br><br>
|
||||
Report any bugs/comments/questions/concerns in the <a href="https://discord.gg/nvP92Mc">Juke Gaming Network Discord</a> or message the author <strong>@juke420</strong>.</p>
|
||||
<!-- ABOUT OVERLAY -->
|
||||
<div id="about-overlay" class="overlay hidden">
|
||||
<div class="overlay-box">
|
||||
<h3>What is Y.A.R.N. HTML5?</h3>
|
||||
<p>Y.A.R.N. (Yet Another Random Narrative) was a collaborative story-writing "parlor game" originally included in GameSpy Arcade in the early 2000s, but removed shortly thereafter due to security issues that were never resolved.</p>
|
||||
<p><strong>Y.A.R.N. HTML5</strong> is a modern remake of (what I can remember from) the original, designed to run in-browser, and with extra features such as (optional) LLM-based bots.</p>
|
||||
<p><strong>The rules are simple:</strong></p>
|
||||
<ul>
|
||||
<li>Players have a limited amount of time to write a sentence, selected by the host at game start.</li>
|
||||
<li>Once all players have submitted their sentences, players vote for their favorite (you cannot vote for your own).</li>
|
||||
<li>All players receive one point for each vote their sentence receives. The player whose sentence receives the most votes is awarded 3 bonus points.</li>
|
||||
<li>In the case of a tie, the longest sentence receives two extra points and the other sentences tied receive 1 point each.</li>
|
||||
<li>The final round is worth double points.</li>
|
||||
<li>When a player reaches the point limit (or the round limit ends), they win the game.</li>
|
||||
</ul>
|
||||
<p>The finished story is saved automatically and can be found under <strong>Past Stories</strong>.<br><br>
|
||||
Report any bugs/comments/questions/concerns in the <a href="https://discord.gg/nvP92Mc">Juke Gaming Network Discord</a> or message the author <strong>@juke420</strong>.</p>
|
||||
<div class="overlay-actions">
|
||||
<button id="close-about">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STORIES OVERLAY -->
|
||||
<div id="stories-overlay" class="overlay hidden">
|
||||
<div class="overlay-box">
|
||||
<h3>Past Stories</h3>
|
||||
<div id="stories-list"><div class="no-rooms">Loading…</div></div>
|
||||
<div id="story-viewer" class="hidden"></div>
|
||||
<div class="overlay-actions">
|
||||
<button id="stories-refresh">Refresh</button>
|
||||
<button id="close-stories">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const roomInput = document.getElementById('room-input');
|
||||
const enterBtn = document.getElementById('enter-btn');
|
||||
const findBtn = document.getElementById('find-btn');
|
||||
const roomListBox = document.getElementById('room-list');
|
||||
const aboutBtn = document.getElementById('about-btn');
|
||||
const aboutBox = document.getElementById('about-overlay');
|
||||
const closeAbout = document.getElementById('close-about');
|
||||
const aboutBtn = document.getElementById('about-btn');
|
||||
const aboutBox = document.getElementById('about-overlay');
|
||||
const closeAbout = document.getElementById('close-about');
|
||||
const storiesBtn = document.getElementById('stories-btn');
|
||||
const storiesBox = document.getElementById('stories-overlay');
|
||||
const closeStories = document.getElementById('close-stories');
|
||||
const storiesRefresh = document.getElementById('stories-refresh');
|
||||
const storiesList = document.getElementById('stories-list');
|
||||
const storyViewer = document.getElementById('story-viewer');
|
||||
|
||||
let findActive = false;
|
||||
let pollId = null;
|
||||
|
||||
function toggleAbout(show){
|
||||
aboutBox.classList.toggle('hidden',!show);
|
||||
aboutBox.classList.toggle('hidden', !show);
|
||||
}
|
||||
aboutBtn.addEventListener('click', () => toggleAbout(true));
|
||||
closeAbout.addEventListener('click', () => toggleAbout(false));
|
||||
aboutBox.addEventListener('click', e => { if (e.target===aboutBox) toggleAbout(false); });
|
||||
aboutBox.addEventListener('click', e => { if (e.target === aboutBox) toggleAbout(false); });
|
||||
|
||||
/* sanitize helper */
|
||||
const esc = str => {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = str;
|
||||
d.textContent = str == null ? '' : String(str);
|
||||
return d.innerHTML;
|
||||
};
|
||||
|
||||
/* ENTER GAME */
|
||||
function goToRoom() {
|
||||
const name = roomInput.value.trim().toLowerCase().replace(/[^a-z0-9-_]/g,'');
|
||||
const name = roomInput.value.trim().toLowerCase().replace(/[^a-z0-9-_]/g, '');
|
||||
if (!name) { alert('Please enter a room name.'); return; }
|
||||
window.location.href = '/game/' + encodeURIComponent(name);
|
||||
}
|
||||
enterBtn.addEventListener('click', goToRoom);
|
||||
roomInput.addEventListener('keydown', e => { if (e.key==='Enter') goToRoom(); });
|
||||
roomInput.addEventListener('keydown', e => { if (e.key === 'Enter') goToRoom(); });
|
||||
|
||||
/* FIND GAME */
|
||||
async function fetchRooms() {
|
||||
try {
|
||||
const r = await fetch('/api/rooms');
|
||||
if (!r.ok) throw new Error();
|
||||
const data = await r.json(); // {rooms:[{name,playerCount},...]}
|
||||
const response = await fetch('/api/rooms');
|
||||
if (!response.ok) throw new Error('bad response');
|
||||
const data = await response.json();
|
||||
renderRooms(data.rooms || []);
|
||||
} catch {
|
||||
renderRooms([]);
|
||||
roomListBox.innerHTML = '<div class="no-rooms">Could not load rooms.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,10 +278,11 @@
|
||||
roomListBox.innerHTML = '<div class="no-rooms">No open rooms right now.</div>';
|
||||
return;
|
||||
}
|
||||
roomListBox.innerHTML = list.map(r => `
|
||||
<div class="room-row" data-room="${esc(r.name)}">
|
||||
<span class="room-name">${esc(r.name)}</span>
|
||||
<span class="room-count">${r.playerCount} player${r.playerCount===1?'':'s'}</span>
|
||||
roomListBox.innerHTML = list.map(room => `
|
||||
<div class="room-row" data-room="${esc(room.name)}">
|
||||
<span class="room-name">${esc(room.name)}</span>
|
||||
<span class="room-status">${esc(room.phase || 'lobby')}</span>
|
||||
<span class="room-count">${room.playerCount} player${room.playerCount === 1 ? '' : 's'}</span>
|
||||
</div>`).join('');
|
||||
|
||||
roomListBox.querySelectorAll('.room-row').forEach(row => {
|
||||
@@ -217,13 +299,63 @@
|
||||
fetchRooms();
|
||||
pollId = setInterval(fetchRooms, 2000);
|
||||
} else {
|
||||
findBtn.textContent = 'Find Game';
|
||||
findBtn.textContent = 'Find Games';
|
||||
clearInterval(pollId);
|
||||
roomListBox.innerHTML = '';
|
||||
}
|
||||
}
|
||||
findBtn.addEventListener('click', toggleFind);
|
||||
|
||||
/* PAST STORIES */
|
||||
async function fetchStories() {
|
||||
storiesList.innerHTML = '<div class="no-rooms">Loading…</div>';
|
||||
storyViewer.classList.add('hidden');
|
||||
try {
|
||||
const response = await fetch('/api/stories');
|
||||
if (!response.ok) throw new Error('bad response');
|
||||
const data = await response.json();
|
||||
renderStories(data.stories || []);
|
||||
} catch {
|
||||
storiesList.innerHTML = '<div class="no-rooms">Could not load stories.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderStories(stories) {
|
||||
if (!stories.length) {
|
||||
storiesList.innerHTML = '<div class="no-rooms">No stories have been saved yet.</div>';
|
||||
return;
|
||||
}
|
||||
storiesList.innerHTML = stories.map(name => `
|
||||
<div class="story-row" data-story="${esc(name)}">
|
||||
<span class="story-name">${esc(name)}</span>
|
||||
<span>open</span>
|
||||
</div>`).join('');
|
||||
|
||||
storiesList.querySelectorAll('.story-row').forEach(row => {
|
||||
row.addEventListener('click', () => openStory(row.dataset.story));
|
||||
});
|
||||
}
|
||||
|
||||
async function openStory(name) {
|
||||
storyViewer.classList.remove('hidden');
|
||||
storyViewer.textContent = 'Loading…';
|
||||
try {
|
||||
const response = await fetch('/api/stories/' + encodeURIComponent(name));
|
||||
if (!response.ok) throw new Error('bad response');
|
||||
storyViewer.textContent = await response.text();
|
||||
} catch {
|
||||
storyViewer.textContent = 'Could not load that story.';
|
||||
}
|
||||
}
|
||||
|
||||
storiesBtn.addEventListener('click', () => {
|
||||
storiesBox.classList.remove('hidden');
|
||||
fetchStories();
|
||||
});
|
||||
closeStories.addEventListener('click', () => storiesBox.classList.add('hidden'));
|
||||
storiesBox.addEventListener('click', e => { if (e.target === storiesBox) storiesBox.classList.add('hidden'); });
|
||||
storiesRefresh.addEventListener('click', fetchStories);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
Reference in new issue
Block a user