Writing a roguelike in C - Part 1 #3
Replies: 1 comment 2 replies
-
|
Yet another interesting video! I'm very sorry for disturbing you again, but I hope you welcome feedback and What I would do: Split the problem in smaller parts, trust the algos: a.) Room placement: Iterative random placement with collision avoidance. So: Create a dungeon layout with rooms that are spatially separated, b.) Corridor generation: Prim's algorithm for Minimum Spanning Trees (MST). Prim’s algorithm is not inherently graphical or 2D. It’s a graph algorithm, Prim’s algo grows the MST one node at a time, always choosing the smallest-weight So when we have the abstract dungeon ready, we can layout the visual paths. c.) Corridor pathfinding: Heuristic L- (and to make it more interesting Z-shaped) Sketch through JavaScript/HTML <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Optimized Dungeon Layout Game</title>
<style>
body { margin: 0; font-family: Arial, sans-serif; }
#canvas { border: 1px solid black; background-color: #f0f0f0; }
#controls { padding: 10px; background-color: #ddd; }
button { margin: 5px; padding: 5px 10px; }
#stats { margin-top: 10px; font-size: 12px; }
</style>
</head>
<body>
<div id="controls">
<button onclick="game.resetDungeon()">Reset Dungeon</button>
<button onclick="game.clearCanvas()">Clear Canvas</button>
<button onclick="game.toggleAnimation()">Toggle Animation</button>
<button onclick="game.clearHistory()">Clear History</button>
<div id="stats"></div>
</div>
<canvas id="canvas" width="800" height="600"></canvas>
<script>
const CONFIG = {
gridSize: 20,
roomCount: 10,
minRoomSize: 30,
maxRoomSize: 60,
minRoomDistance: 40,
maxPlacementAttempts: 50,
dotSpeed: 120, // pixels per second
pauseTime: 1000, // milliseconds
historyDecayTime: 30000, // 30 seconds before rooms become less avoided
maxHistorySize: 50 // max number of visits to remember
};
// pools for memory management
class ObjectPool {
constructor(createFn, resetFn) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];
this.active = [];
}
get() {
let obj = this.pool.pop();
if (!obj) {
obj = this.createFn();
}
this.active.push(obj);
return obj;
}
release(obj) {
const index = this.active.indexOf(obj);
if (index > -1) {
this.active.splice(index, 1);
this.resetFn(obj);
this.pool.push(obj);
}
}
releaseAll() {
while (this.active.length > 0) {
this.release(this.active[0]);
}
}
}
// Vector utility class
class Vector2 {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
set(x, y) {
this.x = x;
this.y = y;
return this;
}
copy(other) {
this.x = other.x;
this.y = other.y;
return this;
}
distance(other) {
return Math.hypot(this.x - other.x, this.y - other.y);
}
normalize() {
const len = Math.hypot(this.x, this.y);
if (len > 0) {
this.x /= len;
this.y /= len;
}
return this;
}
scale(factor) {
this.x *= factor;
this.y *= factor;
return this;
}
add(other) {
this.x += other.x;
this.y += other.y;
return this;
}
}
// Visit history tracker
class VisitHistory {
constructor() {
this.visits = new Map(); // roomIndex -> array of visit times
this.totalVisits = 0;
}
recordVisit(roomIndex) {
const currentTime = performance.now();
if (!this.visits.has(roomIndex)) {
this.visits.set(roomIndex, []);
}
const roomVisits = this.visits.get(roomIndex);
roomVisits.push(currentTime);
this.totalVisits++;
// only recent visits within history size limit!
if (roomVisits.length > CONFIG.maxHistorySize) {
roomVisits.shift();
}
// clean up old visits
this.cleanupOldVisits(currentTime);
}
cleanupOldVisits(currentTime) {
const cutoffTime = currentTime - CONFIG.historyDecayTime;
for (let [roomIndex, visits] of this.visits) {
const filteredVisits = visits.filter(time => time > cutoffTime);
if (filteredVisits.length === 0) {
this.visits.delete(roomIndex);
} else {
this.visits.set(roomIndex, filteredVisits);
}
}
}
getVisitCount(roomIndex) {
return this.visits.has(roomIndex) ? this.visits.get(roomIndex).length : 0;
}
getRecentVisitCount(roomIndex, timeWindow = 10000) {
if (!this.visits.has(roomIndex)) return 0;
const currentTime = performance.now();
const cutoffTime = currentTime - timeWindow;
return this.visits.get(roomIndex).filter(time => time > cutoffTime).length;
}
getUnvisitedRooms(totalRooms) {
const unvisited = [];
for (let i = 0; i < totalRooms; i++) {
if (!this.visits.has(i)) {
unvisited.push(i);
}
}
return unvisited;
}
getLeastVisitedRooms(totalRooms, count = 3) {
const roomCounts = [];
for (let i = 0; i < totalRooms; i++) {
roomCounts.push({
roomIndex: i,
visitCount: this.getVisitCount(i),
recentVisits: this.getRecentVisitCount(i)
});
}
// sort by recent visits first, then by total visits
roomCounts.sort((a, b) => {
if (a.recentVisits !== b.recentVisits) {
return a.recentVisits - b.recentVisits;
}
return a.visitCount - b.visitCount;
});
return roomCounts.slice(0, count).map(item => item.roomIndex);
}
clear() {
this.visits.clear();
this.totalVisits = 0;
}
getStats() {
const uniqueRoomsVisited = this.visits.size;
const totalVisits = Array.from(this.visits.values()).reduce((sum, visits) => sum + visits.length, 0);
return { uniqueRoomsVisited, totalVisits };
}
}
// MST impl. using Prim's algorithm
class MST {
static generate(rooms) {
if (rooms.length === 0) return [];
const edges = [];
const visited = new Set();
const distances = new Map();
// start with first room
visited.add(0);
// add all edges from first room
for (let i = 1; i < rooms.length; i++) {
const dist = rooms[0].position.distance(rooms[i].position);
distances.set(`0-${i}`, dist);
edges.push({ from: 0, to: i, weight: dist });
}
const mstEdges = [];
while (visited.size < rooms.length && edges.length > 0) {
// find minimum weight edge connecting visited to unvisited
edges.sort((a, b) => a.weight - b.weight);
let minEdge = null;
let minIndex = -1;
for (let i = 0; i < edges.length; i++) {
const edge = edges[i];
if (visited.has(edge.from) && !visited.has(edge.to)) {
minEdge = edge;
minIndex = i;
break;
}
}
if (minEdge) {
mstEdges.push(minEdge);
visited.add(minEdge.to);
// add new edges from the newly connected room
for (let i = 0; i < rooms.length; i++) {
if (!visited.has(i)) {
const key = `${minEdge.to}-${i}`;
if (!distances.has(key)) {
const dist = rooms[minEdge.to].position.distance(rooms[i].position);
distances.set(key, dist);
edges.push({ from: minEdge.to, to: i, weight: dist });
}
}
}
edges.splice(minIndex, 1);
} else {
break;
}
}
return mstEdges;
}
}
class Room {
constructor(x, y, id) {
this.position = new Vector2(x, y);
this.id = id;
this.width = Math.floor(Math.random() * (CONFIG.maxRoomSize - CONFIG.minRoomSize + 1)) + CONFIG.minRoomSize;
this.height = Math.floor(Math.random() * (CONFIG.maxRoomSize - CONFIG.minRoomSize + 1)) + CONFIG.minRoomSize;
this.visitCount = 0;
this.lastVisitTime = 0;
}
getConnectionPoint(targetRoom) {
const left = this.position.x - this.width / 2;
const right = this.position.x + this.width / 2;
const top = this.position.y - this.height / 2;
const bottom = this.position.y + this.height / 2;
let nearestX = Math.max(left, Math.min(right, targetRoom.position.x));
let nearestY = Math.max(top, Math.min(bottom, targetRoom.position.y));
// if point is inside room, move to nearest edge
// think this is like "rougelike" style?
if (nearestX > left && nearestX < right && nearestY > top && nearestY < bottom) {
const distances = [
{ pos: left, coord: 'x', val: nearestX - left },
{ pos: right, coord: 'x', val: right - nearestX },
{ pos: top, coord: 'y', val: nearestY - top },
{ pos: bottom, coord: 'y', val: bottom - nearestY }
];
const nearest = distances.reduce((min, curr) => curr.val < min.val ? curr : min);
if (nearest.coord === 'x') {
nearestX = nearest.pos;
} else {
nearestY = nearest.pos;
}
}
return new Vector2(nearestX, nearestY);
}
draw(ctx, visitHistory) {
const visitCount = visitHistory.getVisitCount(this.id - 1);
const recentVisits = visitHistory.getRecentVisitCount(this.id - 1, 5000);
// color based on visit history
let fillColor = '#1E90FF'; // blue
if (visitCount === 0) {
fillColor = '#32CD32'; // green for unvisited
} else if (recentVisits > 0) {
fillColor = '#FF6B6B'; // red for recently visited
} else if (visitCount > 0) {
fillColor = '#FFA500'; // orange for visited but not recent
}
ctx.fillStyle = fillColor;
ctx.fillRect(
this.position.x - this.width / 2,
this.position.y - this.height / 2,
this.width,
this.height
);
// draw checkerboard pattern
ctx.fillStyle = visitCount === 0 ? '#228B22' : '#4682B4';
const cellSize = 5;
for (let i = 0; i < this.width - 4; i += cellSize) {
for (let j = 0; j < this.height - 4; j += cellSize) {
if ((Math.floor(i / cellSize) + Math.floor(j / cellSize)) % 2 === 0) {
ctx.fillRect(
this.position.x - this.width / 2 + 2 + i,
this.position.y - this.height / 2 + 2 + j,
cellSize,
cellSize
);
}
}
}
// draw room ID and visit count
ctx.fillStyle = '#000';
ctx.font = '12px Arial';
ctx.fillText(`R${this.id}`, this.position.x - 10, this.position.y - 5);
if (visitCount > 0) {
ctx.fillText(`(${visitCount})`, this.position.x - 10, this.position.y + 10);
}
}
}
// class with optimised path generation
class Corridor {
constructor(roomA, roomB) {
this.roomA = roomA;
this.roomB = roomB;
this.path = this.generatePath();
this.totalLength = this.calculateLength();
}
generatePath() {
const startPoint = this.roomA.getConnectionPoint(this.roomB);
const endPoint = this.roomB.getConnectionPoint(this.roomA);
const path = [startPoint];
const minOffset = CONFIG.gridSize * 1.5;
const dx = endPoint.x - startPoint.x;
const dy = endPoint.y - startPoint.y;
// path type based on distance
if (Math.abs(dx) > Math.abs(dy)) {
this.addHorizontalPath(path, startPoint, endPoint, minOffset);
} else {
this.addVerticalPath(path, startPoint, endPoint, minOffset);
}
path.push(endPoint);
return path;
}
addHorizontalPath(path, start, end, minOffset) {
const dx = end.x - start.x;
const dy = end.y - start.y;
const horizontalOffset = dx > 0 ? minOffset : -minOffset;
const firstX = start.x + horizontalOffset;
path.push(new Vector2(firstX, start.y));
if (Math.abs(dy) > minOffset) {
const targetX = end.x + (dx > 0 ? -minOffset : minOffset);
path.push(new Vector2(targetX, start.y));
path.push(new Vector2(targetX, end.y));
} else {
const midY = (start.y + end.y) / 2;
path.push(new Vector2(firstX, midY));
const targetX = end.x + (dx > 0 ? -minOffset : minOffset);
path.push(new Vector2(targetX, midY));
path.push(new Vector2(targetX, end.y));
}
}
addVerticalPath(path, start, end, minOffset) {
const dx = end.x - start.x;
const dy = end.y - start.y;
const verticalOffset = dy > 0 ? minOffset : -minOffset;
const firstY = start.y + verticalOffset;
path.push(new Vector2(start.x, firstY));
if (Math.abs(dx) > minOffset) {
const targetY = end.y + (dy > 0 ? -minOffset : minOffset);
path.push(new Vector2(start.x, targetY));
path.push(new Vector2(end.x, targetY));
} else {
const midX = (start.x + end.x) / 2;
path.push(new Vector2(midX, firstY));
const targetY = end.y + (dy > 0 ? -minOffset : minOffset);
path.push(new Vector2(midX, targetY));
path.push(new Vector2(end.x, targetY));
}
}
calculateLength() {
let length = 0;
for (let i = 1; i < this.path.length; i++) {
length += this.path[i].distance(this.path[i - 1]);
}
return length;
}
draw(ctx) {
if (this.path.length < 2) return;
ctx.strokeStyle = '#FF0000';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(this.path[0].x, this.path[0].y);
for (let i = 1; i < this.path.length; i++) {
ctx.lineTo(this.path[i].x, this.path[i].y);
}
ctx.stroke();
}
}
// smart movement system with history-aware pathfinding
class MovementSystem {
constructor(visitHistory) {
this.position = new Vector2();
this.targetPosition = new Vector2();
this.velocity = new Vector2();
this.currentRoomIndex = 0;
this.targetRoomIndex = 0;
this.currentPath = [];
this.pathIndex = 0;
this.isMoving = false;
this.pauseTimer = 0;
this.connectionMap = new Map();
this.visitHistory = visitHistory;
}
initialize(rooms, corridors) {
if (rooms.length === 0) return;
this.buildConnectionMap(rooms, corridors);
this.currentRoomIndex = Math.floor(Math.random() * rooms.length);
this.position.copy(rooms[this.currentRoomIndex].position);
this.targetPosition.copy(this.position);
this.isMoving = false;
this.pauseTimer = 0;
// record initial room visit
this.visitHistory.recordVisit(this.currentRoomIndex);
}
buildConnectionMap(rooms, corridors) {
this.connectionMap.clear();
for (let i = 0; i < rooms.length; i++) {
this.connectionMap.set(i, []);
}
for (let corridor of corridors) {
const roomAIdx = rooms.indexOf(corridor.roomA);
const roomBIdx = rooms.indexOf(corridor.roomB);
this.connectionMap.get(roomAIdx).push({
roomIndex: roomBIdx,
path: corridor.path
});
this.connectionMap.get(roomBIdx).push({
roomIndex: roomAIdx,
path: [...corridor.path].reverse()
});
}
}
update(deltaTime, rooms) {
if (rooms.length < 2) return;
if (this.isMoving) {
this.updateMovement(deltaTime);
} else {
this.pauseTimer += deltaTime;
if (this.pauseTimer >= CONFIG.pauseTime) {
this.startNewJourney(rooms);
}
}
}
updateMovement(deltaTime) {
if (this.pathIndex >= this.currentPath.length) {
this.completeJourney();
return;
}
const target = this.currentPath[this.pathIndex];
const distance = this.position.distance(target);
const moveDistance = CONFIG.dotSpeed * (deltaTime / 1000);
if (distance <= moveDistance) {
this.position.copy(target);
this.pathIndex++;
} else {
// smooth interpolation
const direction = new Vector2(target.x - this.position.x, target.y - this.position.y);
direction.normalize().scale(moveDistance);
this.position.add(direction);
}
}
chooseNextRoom(rooms) {
const connections = this.connectionMap.get(this.currentRoomIndex);
if (!connections || connections.length === 0) return null;
// get unvisited rooms first
const unvisitedRooms = this.visitHistory.getUnvisitedRooms(rooms.length);
// filter connections to only include unvisited rooms, if any exist
let preferredConnections = connections.filter(conn =>
unvisitedRooms.includes(conn.roomIndex)
);
if (preferredConnections.length === 0) {
// if no unvisited rooms are directly connected, use least visited
const leastVisited = this.visitHistory.getLeastVisitedRooms(rooms.length, 5);
preferredConnections = connections.filter(conn =>
leastVisited.includes(conn.roomIndex)
);
}
// if still no preferred connections, use all connections but weight them
if (preferredConnections.length === 0) {
preferredConnections = connections;
}
// weight selection based on visit history
const weightedConnections = preferredConnections.map(conn => {
const visitCount = this.visitHistory.getVisitCount(conn.roomIndex);
const recentVisits = this.visitHistory.getRecentVisitCount(conn.roomIndex, 10000);
// lower weight means higher preference
let weight = 1;
if (visitCount === 0) {
weight = 0.1; // strongly prefer unvisited
} else {
weight = 1 + (visitCount * 0.5) + (recentVisits * 2);
}
return { ...conn, weight };
});
// weighted random selection (inverse weight - lower weight = higher chance)
const totalInverseWeight = weightedConnections.reduce((sum, conn) => sum + (1 / conn.weight), 0);
let random = Math.random() * totalInverseWeight;
for (let conn of weightedConnections) {
random -= (1 / conn.weight);
if (random <= 0) {
return conn;
}
}
// fallback to last connection
return weightedConnections[weightedConnections.length - 1];
}
startNewJourney(rooms) {
const selectedConnection = this.chooseNextRoom(rooms);
if (selectedConnection) {
this.targetRoomIndex = selectedConnection.roomIndex;
this.currentPath = selectedConnection.path;
this.pathIndex = 0;
this.isMoving = true;
this.pauseTimer = 0;
}
}
completeJourney() {
this.isMoving = false;
this.pauseTimer = 0;
this.currentRoomIndex = this.targetRoomIndex;
this.currentPath = [];
this.pathIndex = 0;
// record the visit
this.visitHistory.recordVisit(this.currentRoomIndex);
}
draw(ctx) {
// outer glow
ctx.shadowBlur = 15;
ctx.shadowColor = '#FF6B35';
ctx.fillStyle = '#FF6B35';
ctx.beginPath();
ctx.arc(this.position.x, this.position.y, 8, 0, 2 * Math.PI);
ctx.fill();
// inner bright center
ctx.shadowBlur = 5;
ctx.shadowColor = '#FFFF00';
ctx.fillStyle = '#FFFF00';
ctx.beginPath();
ctx.arc(this.position.x, this.position.y, 4, 0, 2 * Math.PI);
ctx.fill();
// reset shadow
ctx.shadowBlur = 0;
}
}
// separate! rendering concerns
class Renderer {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
}
clear() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
drawGrid() {
this.ctx.strokeStyle = '#ccc';
this.ctx.lineWidth = 1;
for (let x = 0; x < this.canvas.width; x += CONFIG.gridSize) {
this.ctx.beginPath();
this.ctx.moveTo(x, 0);
this.ctx.lineTo(x, this.canvas.height);
this.ctx.stroke();
}
for (let y = 0; y < this.canvas.height; y += CONFIG.gridSize) {
this.ctx.beginPath();
this.ctx.moveTo(0, y);
this.ctx.lineTo(this.canvas.width, y);
this.ctx.stroke();
}
}
render(rooms, corridors, movementSystem, visitHistory) {
this.clear();
this.drawGrid();
// corridors
for (let corridor of corridors) {
corridor.draw(this.ctx);
}
// rooms with history visualisation
for (let room of rooms) {
room.draw(this.ctx, visitHistory);
}
// moving dot
movementSystem.draw(this.ctx);
}
}
class Game {
constructor() {
this.canvas = document.getElementById('canvas');
this.renderer = new Renderer(this.canvas);
this.visitHistory = new VisitHistory();
this.rooms = [];
this.corridors = [];
this.movementSystem = new MovementSystem(this.visitHistory);
this.animationRunning = true;
this.lastTime = 0;
// object pools
this.roomPool = new ObjectPool(
() => new Room(0, 0, 0),
(room) => {
room.position.set(0, 0);
room.id = 0;
}
);
this.vectorPool = new ObjectPool(
() => new Vector2(),
(vector) => vector.set(0, 0)
);
}
initialize() {
this.generateRooms();
this.connectRooms();
this.movementSystem.initialize(this.rooms, this.corridors);
this.startGameLoop();
}
generateRooms() {
// clear existing rooms
this.roomPool.releaseAll();
this.rooms = [];
const usedPositions = [];
for (let i = 0; i < CONFIG.roomCount; i++) {
let attempts = 0;
let validPosition = false;
let x, y;
while (!validPosition && attempts < CONFIG.maxPlacementAttempts) {
x = Math.random() * (this.canvas.width - CONFIG.maxRoomSize) + CONFIG.maxRoomSize / 2;
y = Math.random() * (this.canvas.height - CONFIG.maxRoomSize) + CONFIG.maxRoomSize / 2;
validPosition = true;
for (let pos of usedPositions) {
if (Math.hypot(pos.x - x, pos.y - y) < CONFIG.minRoomDistance) {
validPosition = false;
break;
}
}
attempts++;
}
if (validPosition) {
const room = new Room(x, y, i + 1);
this.rooms.push(room);
usedPositions.push({ x, y });
}
}
}
connectRooms() {
this.corridors = [];
if (this.rooms.length === 0) return;
const mstEdges = MST.generate(this.rooms);
for (let edge of mstEdges) {
const corridor = new Corridor(this.rooms[edge.from], this.rooms[edge.to]);
this.corridors.push(corridor);
}
}
update(deltaTime) {
if (this.animationRunning) {
this.movementSystem.update(deltaTime, this.rooms);
this.updateStats();
}
}
updateStats() {
const stats = this.visitHistory.getStats();
const unvisitedCount = this.visitHistory.getUnvisitedRooms(this.rooms.length).length;
const statsDiv = document.getElementById('stats');
statsDiv.innerHTML = `
Rooms Visited: ${stats.uniqueRoomsVisited}/${this.rooms.length} |
Total Visits: ${stats.totalVisits} |
Unvisited: ${unvisitedCount}
`;
}
render() {
this.renderer.render(this.rooms, this.corridors, this.movementSystem, this.visitHistory);
}
gameLoop(currentTime) {
const deltaTime = currentTime - this.lastTime;
this.lastTime = currentTime;
this.update(deltaTime);
this.render();
requestAnimationFrame((time) => this.gameLoop(time));
}
startGameLoop() {
this.lastTime = performance.now();
this.gameLoop(this.lastTime);
}
resetDungeon() {
this.generateRooms();
this.connectRooms();
this.visitHistory.clear();
this.movementSystem.initialize(this.rooms, this.corridors);
}
clearCanvas() {
this.rooms = [];
this.corridors = [];
this.visitHistory.clear();
this.movementSystem.initialize(this.rooms, this.corridors);
}
clearHistory() {
this.visitHistory.clear();
}
toggleAnimation() {
this.animationRunning = !this.animationRunning;
}
}
// Initialize game
const game = new Game();
game.initialize();
window.game = game;
</script>
<script>
// Event listeners for buttons
document.getElementById('resetDungeon').addEventListener('click', () => {
game.resetDungeon();
});
document.getElementById('clearCanvas').addEventListener('click', () => {
game.clearCanvas();
});
document.getElementById('clearHistory').addEventListener('click', () => {
game.clearHistory();
});
document.getElementById('toggleAnimation').addEventListener('click', () => {
game.toggleAnimation();
});
</script>
</body>
</html>a.) When b.) The c.) Within the
There are some additions to the code, which only is there to show possible Translating code from JavaScript to a pseudo-C style .. Code written STRUCT CONFIG {
INT gridSize;
INT roomCount;
INT minRoomSize;
INT maxRoomSize;
INT minRoomDistance;
INT maxPlacementAttempts;
FLOAT dotSpeed;
INT pauseTime;
INT historyDecayTime;
INT maxHistorySize;
};
// --- Utils ---
CLASS ObjectPool {
FUNCTION createFn;
FUNCTION resetFn;
ARRAY pool;
ARRAY active;
CONSTRUCTOR(createFn, resetFn) {
THIS.createFn = createFn;
THIS.resetFn = resetFn;
// Init pool and active arrays
}
Object get() {
IF (pool IS NOT EMPTY) {
obj = POP FROM pool;
} ELSE {
obj = CALL createFn();
}
ADD obj TO active;
RETURN obj;
}
VOID release(Object obj) {
IF (obj IS IN active) {
REMOVE obj FROM active;
CALL resetFn(obj);
ADD obj TO pool;
}
}
VOID releaseAll() {
WHILE (active IS NOT EMPTY) {
CALL release(FIRST ELEMENT OF active);
}
}
}
CLASS Vector2 {
FLOAT x, y;
CONSTRUCTOR(x = 0, y = 0) {
THIS.x = x;
THIS.y = y;
}
VOID set(FLOAT x, FLOAT y) {
THIS.x = x;
THIS.y = y;
}
VOID copy(Vector2 other) {
THIS.x = other.x;
THIS.y = other.y;
}
FLOAT distance(Vector2 other) {
RETURN SQRT(POW(THIS.x - other.x, 2) + POW(THIS.y - other.y, 2));
}
VOID normalize() {
FLOAT len = distance(NEW Vector2(0,0)); // Euclidian distance from origin
IF (len > 0) {
THIS.x /= len;
THIS.y /= len;
}
}
VOID scale(FLOAT factor) {
THIS.x *= factor;
THIS.y *= factor;
}
VOID add(Vector2 other) {
THIS.x += other.x;
THIS.y += other.y;
}
}
// special addition
CLASS VisitHistory {
MAP<INT, ARRAY<FLOAT>> visits; // roomIndex -> array of visit times
INT totalVisits;
CONSTRUCTOR() {
// Init visits map and totalVisits
}
VOID recordVisit(INT roomIndex) {
FLOAT currentTime = GET_CURRENT_TIME_MS();
IF (visits DOES NOT HAVE roomIndex) {
visits.SET(roomIndex, NEW ARRAY<FLOAT>());
}
ADD currentTime TO visits.GET(roomIndex);
totalVisits++;
// Remove oldest visits, if history size exceeded
IF (visits.GET(roomIndex).LENGTH > CONFIG.maxHistorySize) {
REMOVE FIRST ELEMENT FROM visits.GET(roomIndex);
}
cleanupOldVisits(currentTime);
}
VOID cleanupOldVisits(FLOAT currentTime) {
FLOAT cutoffTime = currentTime - CONFIG.historyDecayTime;
FOR EACH roomIndex, visitsArray IN visits {
FILTER visitsArray TO KEEP ONLY TIMES > cutoffTime;
IF (filteredVisitsArray IS EMPTY) {
REMOVE roomIndex FROM visits;
} ELSE {
visits.SET(roomIndex, filteredVisitsArray);
}
}
}
INT getVisitCount(INT roomIndex) {
RETURN visits.HAS(roomIndex) ? visits.GET(roomIndex).LENGTH : 0;
}
INT getRecentVisitCount(INT roomIndex, INT timeWindowMs) {
// Similar to getVisitCount, but filters for visits within timeWindowMs
}
ARRAY<INT> getUnvisitedRooms(INT totalRooms) {
ARRAY<INT> unvisited;
FOR (INT i = 0; i < totalRooms; i++) {
IF (visits DOES NOT HAVE i) {
ADD i TO unvisited;
}
}
RETURN unvisited;
}
ARRAY<INT> getLeastVisitedRooms(INT totalRooms, INT count) {
ARRAY<STRUCT {INT roomIndex, INT visitCount, INT recentVisits}> roomCounts;
// Populate roomCounts with visit data for all rooms
// Sort roomCounts by recentVisits then by visitCount (ascending)
// Return 'count' number of roomIndexes from the sorted list
}
VOID clear() {
visits.CLEAR();
totalVisits = 0;
}
STRUCT {INT uniqueRoomsVisited, INT totalVisits} getStats() {
// Calc and return stats
}
}
// --- game logic ---
// Minimum Spanning Tree generator (using Prim's Algorithm concept)
CLASS MST {
STATIC ARRAY<STRUCT {INT from, INT to, FLOAT weight}> generate(ARRAY<Room> rooms) {
IF (rooms IS EMPTY) RETURN EMPTY ARRAY;
ARRAY<STRUCT {INT from, INT to, FLOAT weight}> edges;
SET<INT> visited;
MAP<STRING, FLOAT> distances; // Stores distances, to avoid recomputing
ADD 0 TO visited; // Start with first room
// Add initial edges from room 0 to all others
FOR (INT i = 1; i < rooms.LENGTH; i++) {
FLOAT dist = rooms[0].position.distance(rooms[i].position);
distances.SET("0-i", dist);
ADD {from: 0, to: i, weight: dist} TO edges;
}
ARRAY<STRUCT {INT from, INT to, FLOAT weight}> mstEdges;
WHILE (visited.SIZE < rooms.LENGTH AND edges IS NOT EMPTY) {
// Sort edges by weight
SORT edges ASCENDING BY weight;
STRUCT {INT from, INT to, FLOAT weight} minEdge = NULL;
INT minIndex = -1;
FOR (INT i = 0; i < edges.LENGTH; i++) {
STRUCT edge = edges[i];
IF (visited HAS edge.from AND visited DOES NOT HAVE edge.to) {
minEdge = edge;
minIndex = i;
BREAK;
}
}
IF (minEdge IS NOT NULL) {
ADD minEdge TO mstEdges;
ADD minEdge.to TO visited;
// Add new edges from the newly connected room to unvisited rooms
FOR (INT i = 0; i < rooms.LENGTH; i++) {
IF (visited DOES NOT HAVE i) {
STRING key = CONCAT(minEdge.to, "-", i);
IF (distances DOES NOT HAVE key) {
FLOAT dist = rooms[minEdge.to].position.distance(rooms[i].position);
distances.SET(key, dist);
ADD {from: minEdge.to, to: i, weight: dist} TO edges;
}
}
}
REMOVE edge AT minIndex FROM edges;
} ELSE {
BREAK; // No more connections possible
}
}
RETURN mstEdges;
}
}
// A Room in the dungeon
CLASS Room {
Vector2 position;
INT id;
INT width, height;
// (Additional properties for drawing, not! core logic)
CONSTRUCTOR(x, y, id) {
THIS.position = NEW Vector2(x, y);
THIS.id = id;
THIS.width = RANDOM_INT(CONFIG.minRoomSize, CONFIG.maxRoomSize);
THIS.height = RANDOM_INT(CONFIG.minRoomSize, CONFIG.maxRoomSize);
}
Vector2 getConnectionPoint(Room targetRoom) {
// Calc the point on this room's boundary closest to the target room's center.
// If the target room's center is inside this room, move the point to the nearest edge.
// This is a "roguelike" connection style, which I guess we are after.
RETURN NEW Vector2(nearestX, nearestY);
}
VOID draw(Context ctx, VisitHistory visitHistory) {
// If we also want to simulate the rest of the JS code:
// Determine fill color based on visit history (unvisited, recently visited, visited)
// Draw rectangle for room
// Draw checkerboard pattern
// Draw room ID and visit count text
}
}
CLASS Corridor {
Room roomA, roomB;
ARRAY<Vector2> path;
FLOAT totalLength;
CONSTRUCTOR(roomA, roomB) {
THIS.roomA = roomA;
THIS.roomB = roomB;
THIS.path = generatePath();
THIS.totalLength = calculateLength();
}
ARRAY<Vector2> generatePath() {
Vector2 startPoint = roomA.getConnectionPoint(roomB);
Vector2 endPoint = roomB.getConnectionPoint(roomA);
ARRAY<Vector2> path = {startPoint};
// Generate a path with intermediate points using "L" or "Z" shapes
// based on relative positions of startPoint and endPoint.
// This creates "optimised" L-shaped or Z-shaped corridors.
IF (ABS(endPoint.x - startPoint.x) > ABS(endPoint.y - startPoint.y)) {
addHorizontalPath(path, startPoint, endPoint, CONFIG.gridSize * 1.5);
} ELSE {
addVerticalPath(path, startPoint, endPoint, CONFIG.gridSize * 1.5);
}
ADD endPoint TO path;
RETURN path;
}
VOID addHorizontalPath(ARRAY<Vector2> path, Vector2 start, Vector2 end, FLOAT minOffset) {
// Adds points for a horisontal-priority path (e.g., start -> horisontal_turn -> vertical_turn -> end)
}
VOID addVerticalPath(ARRAY<Vector2> path, Vector2 start, Vector2 end, FLOAT minOffset) {
// Adds points for a vertical-priority path (e.g., start -> vertical_turn -> horizontal_turn -> end)
}
FLOAT calculateLength() {
// Sums distances between consecutive points in 'path'
RETURN totalLength;
}
VOID draw(Context ctx) {
// Draw lines connecting points in 'path'
}
}
// If we are after simulation as the JS script ..
// Handles the movement of the "dot" through the dungeon
CLASS MovementSystem {
Vector2 position;
Vector2 targetPosition;
Vector2 velocity;
INT currentRoomIndex;
INT targetRoomIndex;
ARRAY<Vector2> currentPath;
INT pathIndex;
BOOL isMoving;
FLOAT pauseTimer;
MAP<INT, ARRAY<STRUCT {INT roomIndex, ARRAY<Vector2> path}>> connectionMap; // Adjacency list for graph
VisitHistory visitHistory;
CONSTRUCTOR(visitHistory) {
THIS.visitHistory = visitHistory;
// Init position, velocity, etc.
}
VOID initialize(ARRAY<Room> rooms, ARRAY<Corridor> corridors) {
// Build connectionMap from rooms and corridors
// Set initial position to a random room
// Record initial room visit
}
VOID buildConnectionMap(ARRAY<Room> rooms, ARRAY<Corridor> corridors) {
// Populate connectionMap: For each room, list connected rooms and the path to them.
// Ensures bidirectional connections.
}
VOID update(FLOAT deltaTime, ARRAY<Room> rooms) {
IF (isMoving) {
updateMovement(deltaTime);
} ELSE {
pauseTimer += deltaTime;
IF (pauseTimer >= CONFIG.pauseTime) {
startNewJourney(rooms);
}
}
}
VOID updateMovement(FLOAT deltaTime) {
IF (pathIndex IS AT END OF currentPath) {
completeJourney();
RETURN;
}
Vector2 target = currentPath[pathIndex];
FLOAT distance = position.distance(target);
FLOAT moveDistance = CONFIG.dotSpeed * (deltaTime / 1000.0);
IF (distance <= moveDistance) {
position.copy(target);
pathIndex++;
} ELSE {
// Move partially towards target
Vector2 direction = NEW Vector2(target.x - position.x, target.y - position.y);
direction.normalize().scale(moveDistance);
position.add(direction);
}
}
STRUCT {INT roomIndex, ARRAY<Vector2> path} chooseNextRoom(ARRAY<Room> rooms) {
ARRAY<STRUCT {INT roomIndex, ARRAY<Vector2> path}> connections = connectionMap.GET(currentRoomIndex);
IF (connections IS EMPTY) RETURN NULL;
// PRIORITY:
// 1. Unvisited directly connected rooms.
// 2. If none, least visited rooms (globally or within a small set).
// 3. Otherwise, use all connections and apply weights based on visit history
// (lower weight for less visited/less recently visited).
// Select one connection using a weighted random approach.
RETURN selectedConnection;
}
VOID startNewJourney(ARRAY<Room> rooms) {
STRUCT selectedConnection = chooseNextRoom(rooms);
IF (selectedConnection IS NOT NULL) {
targetRoomIndex = selectedConnection.roomIndex;
currentPath = selectedConnection.path;
pathIndex = 0;
isMoving = TRUE;
pauseTimer = 0;
}
}
VOID completeJourney() {
isMoving = FALSE;
pauseTimer = 0;
currentRoomIndex = targetRoomIndex;
currentPath = EMPTY ARRAY;
pathIndex = 0;
visitHistory.recordVisit(currentRoomIndex);
}
VOID draw(Context ctx) {
// Draw the moving dot with glow effects
}
}
CLASS Renderer {
Canvas canvas;
Context ctx;
CONSTRUCTOR(canvas) {
THIS.canvas = canvas;
THIS.ctx = canvas.getContext('2d');
}
VOID clear() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
VOID drawGrid() {
// Draw a background grid on the canvas
}
VOID render(ARRAY<Room> rooms, ARRAY<Corridor> corridors, MovementSystem movementSystem, VisitHistory visitHistory) {
clear();
drawGrid();
FOR EACH corridor IN corridors { corridor.draw(ctx); }
FOR EACH room IN rooms { room.draw(ctx, visitHistory); }
movementSystem.draw(ctx);
}
}
// --- Main ---
// The logic here might be dependent on the exact impl.
// which can vary.
CLASS Game {
Canvas canvas;
Renderer renderer;
VisitHistory visitHistory;
ARRAY<Room> rooms;
ARRAY<Corridor> corridors;
MovementSystem movementSystem;
BOOL animationRunning;
FLOAT lastTime;
// Object Pools (roomPool, vectorPool)
CONSTRUCTOR() {
// Get canvas element
// Init renderer, visitHistory, movementSystem
// Init object pools
// Set initial animation state
}
VOID initialize() {
generateRooms();
connectRooms();
movementSystem.initialize(THIS.rooms, THIS.corridors);
startGameLoop();
}
VOID generateRooms() {
// Clear existing rooms
// Try to place CONFIG.roomCount rooms randomly on the canvas
// Each room must be at least CONFIG.minRoomDistance from others.
// Use CONFIG.maxPlacementAttempts to avoid infinite loops if placement is difficult.
// Use roomPool to get new Room objects.
}
VOID connectRooms() {
// Clear existing corridors
IF (rooms IS EMPTY) RETURN;
ARRAY<STRUCT {INT from, INT to, FLOAT weight}> mstEdges = MST.generate(THIS.rooms);
FOR EACH edge IN mstEdges {
ADD NEW Corridor(rooms[edge.from], rooms[edge.to]) TO corridors;
}
}
VOID update(FLOAT deltaTime) {
IF (animationRunning) {
movementSystem.update(deltaTime, THIS.rooms);
updateStats();
}
}
VOID updateStats() {
// Get stats from visitHistory and update display.
}
VOID render() {
renderer.render(THIS.rooms, THIS.corridors, THIS.movementSystem, THIS.visitHistory);
}
VOID gameLoop(FLOAT currentTime) {
FLOAT deltaTime = currentTime - lastTime;
lastTime = currentTime;
update(deltaTime);
render();
// Request next animation frame (similar to a continuous loop)
REQUEST_ANIMATION_FRAME(gameLoop);
}
VOID startGameLoop() {
lastTime = GET_CURRENT_TIME_MS();
gameLoop(lastTime);
}
VOID resetDungeon() {
generateRooms();
connectRooms();
visitHistory.clear();
movementSystem.initialize(THIS.rooms, THIS.corridors);
}
VOID clearCanvas() {
THIS.rooms = EMPTY ARRAY;
THIS.corridors = EMPTY ARRAY;
visitHistory.clear();
movementSystem.initialize(THIS.rooms, THIS.corridors); // Re-initialise with empty rooms to stop movement
renderer.clear();
updateStats(); // Update stats display to reflect cleared state
}
VOID toggleAnimation() {
animationRunning = !animationRunning;
}
VOID clearHistory() {
visitHistory.clear();
updateStats();
}
}
// Global game instance
Game game;
// On window load (or similar event)
VOID main() {
game = NEW Game();
game.initialize();
}Rgds! |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Video Link Here
Beta Was this translation helpful? Give feedback.
All reactions