johnny961's picture
game v0.111
f28e771
raw
history blame
19 kB
<!DOCTYPE html>
<html>
<head>
<title>Get Me Out! - Apartment Escape Game with Chat</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>
<script src="/static/game/gameState.js"></script>
<style>
/* Layout */
body {
margin: 0;
font-family: Arial, sans-serif;
display: flex;
height: 100vh;
}
#gameContainer {
display: flex;
width: 100%;
height: 100%;
}
#mapSection {
flex: 1;
position: relative;
background: #f5f5f5;
display: flex;
flex-direction: column;
align-items: center;
}
/* We'll place the P5 canvas in here */
.map-wrapper {
margin: 20px;
transform-origin: top left;
}
#chatSection {
width: 400px;
border-left: 1px solid #ddd;
display: flex;
flex-direction: column;
background: white;
}
#chatHistory {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #f8f9fa;
}
#chatControls {
padding: 20px;
border-top: 1px solid #ddd;
background: white;
display: flex;
gap: 10px;
}
.chat-message {
padding: 10px;
margin: 5px 0;
border-radius: 16px;
max-width: 80%;
word-wrap: break-word;
}
.user-message {
background: #007AFF;
color: white;
margin-left: auto;
border-radius: 16px 16px 4px 16px;
}
.assistant-message {
background: #E9E9EB;
color: black;
margin-right: auto;
border-radius: 16px 16px 16px 4px;
}
input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: inherit;
}
button {
padding: 8px 16px;
background: #007AFF;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
.loading-dots {
display: inline-flex;
gap: 4px;
padding: 5px;
margin: 5px 0;
}
.dot {
width: 8px;
height: 8px;
background: #6c757d;
border-radius: 50%;
animation: wave 1.3s linear infinite;
}
.dot:nth-child(2) { animation-delay: -1.1s; }
.dot:nth-child(3) { animation-delay: -0.9s; }
@keyframes wave {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-4px); }
}
/* Modal for API key (if needed) */
#apiKeyModal {
display: none;
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.5);
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
width: 300px;
}
#apiKey {
width: 100%;
margin: 10px 0;
padding: 8px;
}
</style>
</head>
<body>
<div id="gameContainer">
<div id="mapSection">
<div id="mapWrapper" class="map-wrapper">
<!-- P5.js canvas goes here -->
</div>
</div>
<div id="chatSection">
<div id="chatHistory"></div>
<div id="chatControls">
<input type="text" id="prompt" placeholder="Type your message...">
<button onclick="Message()"></button>
</div>
</div>
</div>
<div id="apiKeyModal">
<div class="modal-content">
<h3>Enter Mistral API Key</h3>
<input type="password" id="apiKey" placeholder="Enter your API key">
<button onclick="saveApiKey()">Save</button>
</div>
</div>
<script>
/*
* This script loads ./apt.json to set up the map,
* draws it in p5.js, and provides a chat interface on the right.
*/
// Constants and variables for the map
const CELL_SIZE = 30;
let GRID_COLS = 0;
let GRID_ROWS = 0;
let grid = [];
let rooms = new Map();
let characterPos = null;
let path = [];
let isMoving = false;
let moveInterval = null;
// Initialize game state
const gameState = new GameState();
// Chat variables
let apiKey = localStorage.getItem('mistralApiKey');
let chatMessages = []; // Array to store chat history
// Load apt.json upon page start
async function loadAptJson() {
try {
const response = await fetch('/static/game/apt.json');
if (!response.ok) {
alert("Failed to load apt.json!");
return;
}
const data = await response.json();
// Update local variables from apt.json
GRID_COLS = data.gridCols || 40;
GRID_ROWS = data.gridRows || 20;
grid = data.grid || [];
// Ensure each cell has the color property if it exists in the data
for (let y = 0; y < GRID_ROWS; y++) {
for (let x = 0; x < GRID_COLS; x++) {
if (!grid[y]) grid[y] = [];
if (!grid[y][x]) {
grid[y][x] = { type: 'empty', color: null };
} else if (typeof grid[y][x] === 'string') {
// Handle old format where grid cells were just strings
grid[y][x] = { type: grid[y][x], color: null };
}
}
}
rooms = new Map(data.rooms);
characterPos = data.characterPos || { x: 0, y: 0 };
// Once loaded, initialize the P5 canvas with correct dims
let canvas = createCanvas(GRID_COLS * CELL_SIZE, GRID_ROWS * CELL_SIZE);
canvas.parent('mapWrapper');
adjustScale();
} catch (e) {
console.error("Error loading apt.json:", e);
}
}
function adjustScale() {
const availableWidth = window.innerWidth - 400 - 40; // space for chat + margins
const actualCanvasWidth = GRID_COLS * CELL_SIZE;
const scale = availableWidth / actualCanvasWidth;
const mapWrapper = document.querySelector('#mapSection .map-wrapper');
if (mapWrapper) {
// use CSS zoom or transform
mapWrapper.style.zoom = scale;
}
}
window.addEventListener('resize', adjustScale);
// p5.js setup
function setup() {
// We'll wait to createCanvas until apt.json is loaded
loadAptJson();
}
// p5.js draw loop
function draw() {
if (!grid || grid.length === 0) {
// No grid loaded yet or apt.json not ready
return;
}
background(255);
drawGrid();
drawRooms();
drawWallsAndDoors();
drawPath();
drawCharacter();
}
function drawGrid() {
stroke(200);
for (let x = 0; x <= GRID_COLS; x++) {
line(x * CELL_SIZE, 0, x * CELL_SIZE, GRID_ROWS*CELL_SIZE);
}
for (let y = 0; y <= GRID_ROWS; y++) {
line(0, y * CELL_SIZE, GRID_COLS*CELL_SIZE, y * CELL_SIZE);
}
}
function drawRooms() {
for (let [roomName, cells] of rooms) {
const hue = stringToHue(roomName);
fill(hue, 30, 95, 0.3);
noStroke();
for (let cell of cells) {
rect(cell.x * CELL_SIZE, cell.y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
}
if (cells.length > 0) {
fill(0);
textAlign(CENTER, CENTER);
textSize(10);
text(roomName,
cells[0].x * CELL_SIZE + CELL_SIZE/2,
cells[0].y * CELL_SIZE + CELL_SIZE/2
);
}
}
}
function drawWallsAndDoors() {
for (let y = 0; y < GRID_ROWS; y++) {
for (let x = 0; x < GRID_COLS; x++) {
const cell = grid[y][x];
if (cell.type === 'wall') {
fill(0);
noStroke();
rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
} else if (cell.type === 'door') {
fill(139, 69, 19); // Brown color for doors
noStroke();
rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
} else if (cell.color) {
noStroke();
switch(cell.color) {
case 'yellow':
fill('#ffeb3b');
break;
case 'blue':
fill('#2196f3');
break;
case 'green':
fill('#4caf50');
break;
case 'red':
fill('#f44336');
break;
}
rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
}
}
}
}
function drawPath() {
if (path.length > 0 && isMoving) {
noFill();
stroke(0, 255, 0);
strokeWeight(2);
line(
characterPos.x * CELL_SIZE + CELL_SIZE/2,
characterPos.y * CELL_SIZE + CELL_SIZE/2,
path[0].x * CELL_SIZE + CELL_SIZE/2,
path[0].y * CELL_SIZE + CELL_SIZE/2
);
for (let i = 0; i < path.length - 1; i++) {
line(
path[i].x * CELL_SIZE + CELL_SIZE/2,
path[i].y * CELL_SIZE + CELL_SIZE/2,
path[i + 1].x * CELL_SIZE + CELL_SIZE/2,
path[i + 1].y * CELL_SIZE + CELL_SIZE/2
);
}
strokeWeight(1);
}
}
function drawCharacter() {
if (characterPos) {
textSize(CELL_SIZE * 0.8);
textAlign(CENTER, CENTER);
text('👧',
characterPos.x * CELL_SIZE + CELL_SIZE/2,
characterPos.y * CELL_SIZE + CELL_SIZE/2
);
}
}
function stringToHue(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
return hash % 360;
}
// Simple BFS for pathfinding
function findPath(start, end) {
const queue = [[start]];
const visited = new Set();
const key = pos => `${pos.x},${pos.y}`;
visited.add(key(start));
while (queue.length > 0) {
const currentPath = queue.shift();
const current = currentPath[currentPath.length - 1];
if (current.x === end.x && current.y === end.y) {
return currentPath;
}
const neighbors = [
{ x: current.x, y: current.y - 1 },
{ x: current.x+1, y: current.y },
{ x: current.x, y: current.y+1 },
{ x: current.x-1, y: current.y }
];
for (const next of neighbors) {
if (next.x < 0 || next.x >= GRID_COLS || next.y < 0 || next.y >= GRID_ROWS) continue;
if (visited.has(key(next))) continue;
const cell = grid[next.y][next.x];
if (cell.type === 'wall') continue;
visited.add(key(next));
queue.push([...currentPath, next]);
}
}
return [];
}
function moveToRoom(roomName) {
const targetRoom = Array.from(rooms.entries())
.find(([name]) => name.toLowerCase() === roomName.toLowerCase());
if (!targetRoom || !characterPos) return false;
const [_, cells] = targetRoom;
if (cells.length === 0) return false;
path = findPath(characterPos, cells[0]);
if (path.length > 0) {
isMoving = true;
moveCharacterAlongPath();
return true;
}
return false;
}
function moveCharacterAlongPath() {
if (moveInterval) clearInterval(moveInterval);
moveInterval = setInterval(() => {
if (path.length === 0) {
isMoving = false;
clearInterval(moveInterval);
return;
}
const nextPos = path.shift();
characterPos = nextPos;
}, 200);
}
/* Chat / Mistral-related code */
function createLoadingIndicator() {
const loadingDiv = document.createElement('div');
loadingDiv.className = 'chat-message assistant-message';
loadingDiv.innerHTML = `
<div class="loading-dots">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
`;
return loadingDiv;
}
function addMessageToChat(role, content) {
const chatHistory = document.getElementById('chatHistory');
const messageDiv = document.createElement('div');
messageDiv.className = `chat-message ${role}-message`;
messageDiv.textContent = content;
chatHistory.appendChild(messageDiv);
chatHistory.scrollTop = chatHistory.scrollHeight;
// Store message in chat history
chatMessages.push({ role, content });
}
async function Message() {
const prompt = document.getElementById('prompt').value.trim();
if (!prompt) return;
if (!apiKey) {
alert('Please enter your Mistral API key first');
document.getElementById('apiKeyModal').style.display = 'flex';
return;
}
addMessageToChat('user', prompt);
document.getElementById('prompt').value = '';
const chatHistory = document.getElementById('chatHistory');
const loadingIndicator = createLoadingIndicator();
chatHistory.appendChild(loadingIndicator);
try {
// Get last 5 messages from chat history
const recentMessages = chatMessages.slice(-5);
// Prepare messages array for Mistral
const messages = [
{ role: 'system', content: gameState.getPrompt() },
...recentMessages,
{ role: 'user', content: prompt }
];
const response = await fetch('https://api.mistral.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: 'mistral-large-latest',
messages: messages
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Mistral response:', data.choices[0].message.content);
const assistantResponse = data.choices[0].message.content || "";
try {
const jsonStart = assistantResponse.indexOf('{');
const jsonEnd = assistantResponse.lastIndexOf('}') + 1;
const jsonContent = assistantResponse.substring(jsonStart, jsonEnd);
const jsonResponse = JSON.parse(jsonContent);
console.log('Parsed JSON response:', jsonResponse);
if (jsonResponse.textMessage) {
addMessageToChat('assistant', jsonResponse.textMessage);
}
if (jsonResponse.action === 'go' && jsonResponse.to) {
moveToRoom(jsonResponse.to);
}
} catch (e) {
console.error('Error parsing JSON from response:', e);
addMessageToChat('assistant', 'Sorry, I had trouble understanding that response.');
}
} catch (error) {
addMessageToChat('assistant', `Error: ${error.message}`);
} finally {
loadingIndicator.remove();
}
}
function saveApiKey() {
const key = document.getElementById('apiKey').value.trim();
if (key) {
apiKey = key;
localStorage.setItem('mistralApiKey', key);
document.getElementById('apiKeyModal').style.display = 'none';
}
}
// Allow ing message with Enter
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('prompt').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
Message();
}
});
if (!apiKey) {
document.getElementById('apiKeyModal').style.display = 'flex';
}
});
</script>
</body>
</html>