Basic bot support and option to change opening text
This commit is contained in:
parent
7bc9d67544
commit
499a5954f5
|
|
@ -0,0 +1,37 @@
|
|||
const RandomBot = require('./RandomBot');
|
||||
const LLMBot = require('./LLMBot');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class BotFactory {
|
||||
static createBot(type) {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'random':
|
||||
return new RandomBot();
|
||||
case 'llm':
|
||||
return new LLMBot();
|
||||
default:
|
||||
throw new Error(`Unknown bot type: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
static generateBotName() {
|
||||
const names = fs.readFileSync(path.join(__dirname, 'bot_names.txt'), 'utf8')
|
||||
.split('\n')
|
||||
.map(name => name.trim())
|
||||
.filter(name => name.length > 0);
|
||||
|
||||
if (names.length === 0) {
|
||||
return 'DefaultBot'; // Fallback if no names are found
|
||||
}
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * names.length);
|
||||
return names[randomIndex];
|
||||
}
|
||||
|
||||
static getAvailableBotTypes() {
|
||||
return ['random'];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BotFactory;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
const { LLM } = require('@themaximalist/llm.js'); // or wherever your LLM package lives
|
||||
|
||||
class LLMBot {
|
||||
constructor() {
|
||||
// One instance keeps the whole conversation in memory
|
||||
this.llm = new LLM();
|
||||
|
||||
// Prompt fragments we’ll reuse
|
||||
this.writePrompt =
|
||||
`You are playing a collaborative storytelling game.
|
||||
Continue the following story with exactly ONE short, creative sentence.
|
||||
Do NOT repeat what’s already written.`;
|
||||
|
||||
this.votePrompt =
|
||||
`You are playing a collaborative storytelling game.
|
||||
Below is the story so far, followed by a list of possible next sentences.
|
||||
Reply with ONLY the number (0-indexed) of the sentence you like best.`;
|
||||
|
||||
this.banterPrompt =
|
||||
`You are a playful, slightly sarcastic AI taking part in a writing-game chat.
|
||||
Keep it short, fun, and on-topic.`;
|
||||
}
|
||||
|
||||
async Write(story_so_far) {
|
||||
const msg = `${this.writePrompt}\n\nStory so far:\n${story_so_far}`;
|
||||
return (await this.llm.chat(msg)).trim();
|
||||
}
|
||||
|
||||
async Vote(story_so_far, choices) {
|
||||
const choicesBlock = choices.map((c, i) => `${i}: ${c}`).join('\n');
|
||||
const msg = `${this.votePrompt}\n\nStory:\n${story_so_far}\n\nChoices:\n${choicesBlock}`;
|
||||
const reply = (await this.llm.chat(msg)).trim();
|
||||
// extract first digit found, fallback to 0
|
||||
const match = reply.match(/\d+/);
|
||||
return match ? parseInt(match[0], 10) : 0;
|
||||
}
|
||||
|
||||
async Banter(chat_so_far) {
|
||||
const msg = `${this.banterPrompt}\n\nChat so far:\n${chat_so_far}`;
|
||||
return (await this.llm.chat(msg)).trim();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LLMBot;
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
class RandomBot {
|
||||
constructor() {
|
||||
this.writePhrases = [
|
||||
" Suddenly, a mysterious figure appeared from the shadows.",
|
||||
" The ground began to shake violently beneath their feet.",
|
||||
" In the distance, an eerie sound echoed through the valley.",
|
||||
" A bright light flashed, blinding everyone momentarily.",
|
||||
" The ancient artifact started glowing with an otherworldly energy."
|
||||
];
|
||||
|
||||
this.votePhrases = [
|
||||
"I think we should proceed with caution.",
|
||||
"Let's take the bold approach!",
|
||||
"The safe path seems best right now.",
|
||||
"We should explore further before deciding.",
|
||||
"My instincts tell me to go left."
|
||||
];
|
||||
|
||||
this.banterPhrases = [
|
||||
"Did anyone else hear that?",
|
||||
"I have a good feeling about this!",
|
||||
"Not sure what's happening, but I'm excited!",
|
||||
"Anyone else getting hungry?",
|
||||
"Remember when we used to play simple games?",
|
||||
"What's the worst that could happen?",
|
||||
"I wonder if there are cookies in this game.",
|
||||
"Do bots dream of electric sheep?",
|
||||
"Is this thing on?",
|
||||
"I'm having a great time, how about you?"
|
||||
];
|
||||
}
|
||||
|
||||
Write(story_so_far) {
|
||||
const randomIndex = Math.floor(Math.random() * this.writePhrases.length);
|
||||
return this.writePhrases[randomIndex];
|
||||
}
|
||||
|
||||
Vote(story_so_far, choices) {
|
||||
// Return a random choice number (0-indexed)
|
||||
const randomChoice = Math.floor(Math.random() * choices.length);
|
||||
return randomChoice;
|
||||
}
|
||||
|
||||
Banter(chat_so_far) {
|
||||
const randomIndex = Math.floor(Math.random() * this.banterPhrases.length);
|
||||
return this.banterPhrases[randomIndex];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RandomBot;
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
PunBot3000
|
||||
SarcasmBot
|
||||
DadJokeBot
|
||||
WattBot
|
||||
ChaiBot
|
||||
NachoBot
|
||||
EyeBot
|
||||
FryBot
|
||||
GuacBot
|
||||
TacoBot
|
||||
ByteMeBot
|
||||
CtrlAltBot
|
||||
DeCAFbot
|
||||
SirMixABot
|
||||
HolyBot
|
||||
BrewBot
|
||||
KnotBot
|
||||
WheyBot
|
||||
YodaBot
|
||||
ObiWanBot
|
||||
RickRollBot
|
||||
NeverGonnaBot
|
||||
SuspiciousBot
|
||||
TaskBot
|
||||
MemeLordBot
|
||||
YeetBot
|
||||
SkibidiBot
|
||||
RizzBot
|
||||
OhioBot
|
||||
GyattBot
|
||||
SoupBot
|
||||
BDEBot
|
||||
RatioBot
|
||||
LurkingBot
|
||||
GhostingBot
|
||||
SnackBot
|
||||
ExistentialBot
|
||||
ProcrastinBot
|
||||
FOMO_Bot
|
||||
ImpostorBot
|
||||
SendingMemesBot
|
||||
PepeBot
|
||||
|
|
@ -11,8 +11,10 @@
|
|||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@themaximalist/llm.js": "^1.0.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"express": "^5.2.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"socket.io": "^4.8.3",
|
||||
"uuid": "^13.0.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>YARN 2026 – Spin Your Story!</title>
|
||||
<title>YARN 2026 – Spin a Story!</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-main: #4b0082;
|
||||
|
|
@ -561,6 +561,14 @@
|
|||
<label>Time per Round (sec):</label>
|
||||
<input type="number" id="time-limit" value="${gameState.gameSettings.timeLimit}" min="15" max="180">
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label>Bots:</label>
|
||||
<input type="number" id="bot-count" value="${gameState.gameSettings.botCount}" min="0" max="4">
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label>Starting Text:</label>
|
||||
<input type="text" id="start-text" value="${gameState.gameSettings.startText}">
|
||||
</div>
|
||||
<button id="start-game-btn">Start Game</button>
|
||||
`;
|
||||
|
||||
|
|
@ -748,10 +756,14 @@
|
|||
function startGame() {
|
||||
const scoreLimit = parseInt(document.getElementById('score-limit').value) || 100;
|
||||
const timeLimit = parseInt(document.getElementById('time-limit').value) || 60;
|
||||
const botCount = parseInt(document.getElementById('bot-count').value) || 0;
|
||||
const startText = document.getElementById('start-text').value;
|
||||
|
||||
socket.emit('startGame', {
|
||||
scoreLimit: Math.max(10, Math.min(9999, scoreLimit)),
|
||||
timeLimit: Math.max(15, Math.min(600, timeLimit))
|
||||
timeLimit: Math.max(15, Math.min(600, timeLimit)),
|
||||
botCount: Math.max(0, Math.min(5, botCount)),
|
||||
startText
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
82
server.js
82
server.js
|
|
@ -5,6 +5,7 @@ const path = require('path');
|
|||
const cookieParser = require('cookie-parser');
|
||||
//const { v4: uuidv4 } = require('uuid');
|
||||
const { randomUUID } = require('node:crypto');
|
||||
const BotFactory = require('./bots/BotFactory');
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
|
|
@ -25,8 +26,10 @@ class YarnGame {
|
|||
this.gameSettings = {
|
||||
scoreLimit: 50,
|
||||
timeLimit: 60,
|
||||
botCount: 0
|
||||
botCount: 0,
|
||||
startText: "It was a dark and stormy night..."
|
||||
};
|
||||
this.bots = [];
|
||||
this.players = []; // { player_id, name, score, socketId }
|
||||
this.disconnectedPlayers = []; // So people can keep their score when they reconnect
|
||||
this.yarnStory = []; // { player: "", str: "" }
|
||||
|
|
@ -78,20 +81,23 @@ class YarnGame {
|
|||
removePlayer(playerId) {
|
||||
const index = this.players.findIndex(p => p.player_id === playerId);
|
||||
if (index !== -1) {
|
||||
console.log(`Player ${this.players[index].name} dropped.`)
|
||||
const removed = this.players.splice(index, 1);
|
||||
this.disconnectedPlayers.push(removed[0]);
|
||||
|
||||
// Assign new host if host left
|
||||
if (this.gameHost === playerId && this.players.length > 0) {
|
||||
this.gameHost = this.players[0].player_id;
|
||||
if (this.gameHost === playerId && this.getHumanPlayers().length > 0) {
|
||||
const human_players = this.getHumanPlayers();
|
||||
this.gameHost = human_players[0].player_id;
|
||||
}
|
||||
}
|
||||
if (this.players.length > 0) {
|
||||
console.log(`Player ${this.players[index].name} dropped.`)
|
||||
}
|
||||
return this.players.length;
|
||||
}
|
||||
|
||||
getHumanPlayers() {
|
||||
return this.players.filter(p => !this.bots.some(b => b.bot_id === p.player_id));
|
||||
}
|
||||
|
||||
getPlayer(playerId) {
|
||||
return this.players.find(p => p.player_id === playerId);
|
||||
}
|
||||
|
|
@ -107,19 +113,49 @@ class YarnGame {
|
|||
}
|
||||
}
|
||||
|
||||
startBots(botCount) {
|
||||
console.log("Creating bots...")
|
||||
for (let i = 0; i < botCount; i++) {
|
||||
const bot_id = randomUUID()
|
||||
const bot = BotFactory.createBot("random");
|
||||
this.bots.push({ bot_id, bot });
|
||||
const bot_player = {
|
||||
player_id: bot_id,
|
||||
name: BotFactory.generateBotName(),
|
||||
score: 0,
|
||||
socketId: null
|
||||
};
|
||||
this.players.push(bot_player);
|
||||
console.log(`Created bot with UUID ${bot_id}`)
|
||||
}
|
||||
}
|
||||
|
||||
startGame(settings) {
|
||||
this.gameSettings = { ...this.gameSettings, ...settings };
|
||||
this.startBots(this.gameSettings.botCount);
|
||||
this.inProgress = true;
|
||||
this.yarnStory = [{ player: "Narrator", str: "It was a dark and stormy night..." }];
|
||||
this.yarnStory = [{ player: "Narrator", str: this.gameSettings.startText }];
|
||||
this.players.forEach(p => p.score = 0);
|
||||
this.startWritingPhase();
|
||||
}
|
||||
|
||||
processBotWriting() {
|
||||
for (const botObj of this.bots) {
|
||||
const botText = botObj.bot.Write(this.getStoryText());
|
||||
this.submitEntry(botObj.bot_id, botText)
|
||||
}
|
||||
}
|
||||
|
||||
startWritingPhase() {
|
||||
this.currentPhase = 'writing';
|
||||
this.round_data = [];
|
||||
this.submittedPlayers = new Set();
|
||||
this.timeRemaining = this.gameSettings.timeLimit;
|
||||
this.processBotWriting()
|
||||
}
|
||||
|
||||
getStoryText() {
|
||||
return this.yarnStory.map(entry => entry.str).join('');
|
||||
}
|
||||
|
||||
submitEntry(playerId, entryText) {
|
||||
|
|
@ -140,10 +176,18 @@ class YarnGame {
|
|||
return this.submittedPlayers.size >= this.players.length;
|
||||
}
|
||||
|
||||
processBotVoting() {
|
||||
for (const botObj of this.bots) {
|
||||
const botVote = botObj.bot.Vote(this.getStoryText(), this.round_data);
|
||||
this.submitVote(botObj.bot_id, botVote)
|
||||
}
|
||||
}
|
||||
|
||||
startVotingPhase() {
|
||||
this.currentPhase = 'voting';
|
||||
this.votedPlayers = new Set();
|
||||
this.timeRemaining = 30; // 30 seconds for voting
|
||||
this.processBotVoting()
|
||||
}
|
||||
|
||||
submitVote(playerId, entryIndex) {
|
||||
|
|
@ -203,9 +247,17 @@ class YarnGame {
|
|||
return null;
|
||||
}
|
||||
|
||||
purgeBots() {
|
||||
for (const botObj of this.bots) {
|
||||
this.removePlayer(botObj.bot_id)
|
||||
}
|
||||
this.bots = [];
|
||||
}
|
||||
|
||||
resetGame() {
|
||||
this.inProgress = false;
|
||||
this.currentPhase = 'lobby';
|
||||
this.purgeBots();
|
||||
this.round_data = [];
|
||||
this.yarnStory = [];
|
||||
this.submittedPlayers = new Set();
|
||||
|
|
@ -216,6 +268,18 @@ class YarnGame {
|
|||
}
|
||||
}
|
||||
|
||||
botBanter() {
|
||||
for (const botObj of this.bots) {
|
||||
if (Math.random() > 0.8) {
|
||||
console.log("Bot bantering")
|
||||
const botBanter = botObj.bot.Banter(this.getStoryText());
|
||||
console.log(botBanter)
|
||||
const chatMsg = this.addChatMessage(botObj.bot_id, botBanter)
|
||||
io.to(this.roomName).emit('newChatMessage', chatMsg); //I don't like using io calls here but chat is weird
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addChatMessage(playerId, msg) {
|
||||
const player = this.getPlayer(playerId);
|
||||
const chatMsg = {
|
||||
|
|
@ -331,7 +395,7 @@ io.on('connection', (socket) => {
|
|||
|
||||
const game = games[currentRoom];
|
||||
if (!game || game.gameHost !== playerId) return;
|
||||
if (game.players.length < 2) {
|
||||
if (game.players.length < 2 && settings.botCount == 0) {
|
||||
socket.emit('error', { message: 'Need at least 2 players to start' });
|
||||
return;
|
||||
}
|
||||
|
|
@ -422,9 +486,9 @@ io.on('connection', (socket) => {
|
|||
io.to(currentRoom).emit('playersUpdate', game.getState().players);
|
||||
io.to(currentRoom).emit('gameState', game.getState());
|
||||
|
||||
if (remaining === 0 || game.bots.length === game.players.length) {
|
||||
// Clean up empty rooms
|
||||
console.log(`Removing empty room ${currentRoom}`)
|
||||
if (remaining === 0) {
|
||||
if (game.timer) clearInterval(game.timer);
|
||||
delete games[currentRoom];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue