thinstuff

This commit is contained in:
Kim Ravn Hansen
2025-09-10 22:37:54 +02:00
parent ba293d08b3
commit 8c196bb6a1
13 changed files with 350 additions and 213 deletions

View File

@@ -1,48 +0,0 @@
<!-- CHATGPT EXAMPLE -->
<!-- 1. Get the width of the container (in pixels). -->
<!-- 2. Get the width of a single character in the monospaced font (this can be done by creating a temporary element with the same font and measuring its width).-->
<!-- 3. Divide the container's width by the character's width to get the number of characters. -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Measure Div Width in Characters</title>
<style>
.monospaced-div {
font-family: "Courier New", Courier, monospace; /* Monospaced font */
width: 360px; /* Example width in pixels */
border: 1px solid #333;
padding: 10px;
}
</style>
</head>
<body>
<div class="monospaced-div">This is a div with monospaced text.</div>
<script>
function getMonospacedCharCount(div) {
// Create a temporary span to get the width of one character
const testChar = document.createElement("span");
testChar.textContent = "0"; // Monospaced fonts use "0" for width
testChar.style.fontFamily = window.getComputedStyle(div).fontFamily;
testChar.style.visibility = "hidden"; // Hide the element
document.body.appendChild(testChar);
const charWidth = testChar.offsetWidth; // Get width of a single character
document.body.removeChild(testChar); // Remove the test element
// Get the width of the div and calculate how many characters fit
const divWidth = div.offsetWidth;
// Return the number of characters that fit in the div width
return Math.floor(divWidth / charWidth);
}
const div = document.querySelector(".monospaced-div");
const charCount = getMonospacedCharCount(div);
console.log("Number of characters the div can hold:", charCount);
</script>
</body>
</html>

View File

@@ -21,7 +21,7 @@ They never trigger quests or events where they go, but:
- they can interact with adventurers (they are quite aggressive, and may attack unprovoked, maybe sneak past)
- they can interact with each other, but mostly do so if there are PCs nearby.
# Attrition
# ATTRITION
Even when a player is offline, their characters have to pay rent on their homes
or room and board in an inn, or they can chance it in the wilderness.
@@ -30,3 +30,20 @@ If they run out of money or rations, there is a small chance each day that the
characters will be garbage collected.
The sum that needs paying while offline not very large though.
# CHAT SPELL
You can buy a spell that lets you initiate a secure and encrypted group chat
with other players.
The person who casts the spell generates a private key and sends the public key
to the others in the chat. Each recipient then generates a one-time symmetric
key and sends it securely (via the caster's public key) to the caster. The
caster then generates a "group chat key" and sends it to each recipient via
their one-time key.
Any chats via the spell from then on is encrypted with the "group chat key".
All parties throw away the group chat key when the spell ends.
Each group chat has a name.

View File

@@ -8,6 +8,7 @@
*/
import { isIdSane, miniUid } from "../utils/id.js";
import { Xorshift32 } from "../utils/random.js";
import { Character } from "./character.js";
import { ItemAttributes, ItemBlueprint } from "./item.js";
import { Player } from "./player.js";
@@ -33,6 +34,24 @@ export class Game {
*/
_players = new Map();
/** @protected @type {Xorshift32} */
_rng;
/** @type {Xorshift32} */
get rng() {
return this._rng;
}
/** @param {number} rngSeed Seed number used for randomization */
constructor(rngSeed) {
if (!Number.isInteger(rngSeed)) {
throw new Error("rngSeed must be an integer");
}
this._rng = new Xorshift32(rngSeed);
}
getPlayer(username) {
return this._players.get(username);
}

View File

@@ -30,6 +30,7 @@
"prettier": {
"tabWidth": 4,
"printWidth": 120,
"quoteProps": "consistent",
"singleQuote": false,
"trailingComma": "all",
"tabWidth": 4,

View File

@@ -73,6 +73,11 @@ export class CharacterSeeder {
// Rolling skills
c.name =
this.game.rng.oneOf("sir", "madam", "mister", "miss", "", "", "") +
" random " +
this.game.rng.get().toString();
c.awareness = roll.d6() + 2;
c.grit = roll.d6() + 2;
c.knowledge = roll.d6() + 2;
@@ -126,6 +131,10 @@ export class CharacterSeeder {
}
this.applyFoundation(c);
console.log(c);
return c;
}
/**
@@ -141,7 +150,9 @@ export class CharacterSeeder {
createParty(player, partySize) {
//
for (let i = 0; i < partySize; i++) {
const character = this.createCharacter(player);
player.addCharacter(
this.createCharacter(player), //
);
}
}
@@ -149,9 +160,9 @@ export class CharacterSeeder {
* @param {Character} c
* @param {string|number} Foundation to add to character
*/
applyFoundation(c, foundation = "random") {
applyFoundation(c, foundation = ":random") {
switch (foundation) {
case "random":
case ":random":
return this.applyFoundation(c, roll.dice(3));
break;
@@ -193,12 +204,12 @@ export class CharacterSeeder {
c, //
":armor.light.leather",
":weapon.light.sickle",
":kits.poisoners_kit",
":kits.healers_kit",
":kit.poisoners_kit",
":kit.healers_kit",
);
this.addSkillsToCharacter(
c, //
":armor.light.leather",
":armor.light.sleather",
":armor.light.hide",
":weapon.light.sickle",
);
@@ -231,6 +242,7 @@ export class CharacterSeeder {
":weapon.light.rapier",
":weapon.light.dagger",
);
break;
/*

View File

@@ -12,9 +12,9 @@ import { PlayerSeeder } from "./playerSeeder.js";
*/
export class GameSeeder {
/** @returns {Game} */
createGame() {
createGame(rngSeed) {
/** @protected @constant @readonly @type {Game} */
this.game = new Game();
this.game = new Game(rngSeed);
this.work(); // Seeding may take a bit, so let's defer it so we can return early.

View File

@@ -31,7 +31,6 @@ export class ItemSeeder {
itemSlots: 0.5,
damage: 3,
melee: true,
skills: [":weapon.light"],
ranged: true,
specialEffect: ":effect.weapon.fast",
});
@@ -41,7 +40,6 @@ export class ItemSeeder {
description: "For cutting nuts, and branches",
itemSlots: 1,
damage: 4,
skills: [":weapon.light"],
specialEffect: ":effect.weapon.sickle",
});
@@ -50,11 +48,14 @@ export class ItemSeeder {
description: "Spikes with gauntlets on them!",
itemSlots: 1,
damage: 5,
skills: [
// Spiked gauntlets are :Weird so you must be specially trained to use them.
// This is done by having a skill that exactly matches the weapon's blueprintId
":weapon.weird.spiked_gauntlets",
],
specialEffect: "TBD",
});
this.game.addItemBlueprint(":weapon.light.rapier", {
name: "Rapier",
description: "Fancy musketeer sword",
itemSlots: 1,
damage: 5,
specialEffect: "TBD",
});
@@ -70,7 +71,6 @@ export class ItemSeeder {
description: "Padded and hardened leather with metal stud reinforcement",
itemSlots: 3,
specialEffect: "TBD",
skills: [":armor.light"],
armorHitPoints: 10,
});
this.game.addItemBlueprint(":armor.light.leather", {
@@ -78,7 +78,6 @@ export class ItemSeeder {
description: "Padded and hardened leather",
itemSlots: 2,
specialEffect: "TBD",
skills: [":armor.light"],
armorHitPoints: 6,
});

View File

@@ -9,191 +9,177 @@ import { AuthState } from "./states/authState.js";
import { GameSeeder } from "./seeders/gameSeeder.js";
import { Config } from "./config.js";
// __ __ _ _ ____ ____
// | \/ | | | | _ \ / ___| ___ _ ____ _____ _ __
// | |\/| | | | | | | | \___ \ / _ \ '__\ \ / / _ \ '__|
// | | | | |_| | |_| | ___) | __/ | \ V / __/ |
// |_| |_|\___/|____/ |____/ \___|_| \_/ \___|_|
// -----------------------------------------------------
class MudServer {
constructor() {
/** @type {Game} */
this.game = new GameSeeder().createGame();
}
/** @type {Xorshift32} */
rng;
// ____ ___ _ _ _ _ _____ ____ _____ _____ ____
// / ___/ _ \| \ | | \ | | ____/ ___|_ _| ____| _ \
// | | | | | | \| | \| | _|| | | | | _| | | | |
// | |__| |_| | |\ | |\ | |__| |___ | | | |___| |_| |
// \____\___/|_| \_|_| \_|_____\____| |_| |_____|____/
//------------------------------------------------------
// Handle New Socket Connections
//------------------------------
/** @param {WebSocket} websocket */
onConnectionEstabished(websocket) {
console.log("New connection established");
const session = new Session(websocket, this.game);
session.sendSystemMessage("dev", true);
/** @param {number?} rngSeed seed for the pseudo-random number generator. */
constructor(rngSeed = undefined) {
/** @type {Game} */
this.game = new GameSeeder().createGame(rngSeed || Date.now());
}
// ____ _ ___ ____ _____
// / ___| | / _ \/ ___|| ____|
// | | | | | | | \___ \| _|
// | |___| |__| |_| |___) | |___
// \____|_____\___/|____/|_____|
//-------------------------------
// Handle Socket Closing
//----------------------
websocket.on("close", () => {
if (!session.player) {
console.info("A player without a session disconnected");
return;
}
//-------------
// TODO
//-------------
// Handle player logout (move the or hide their characters)
//
// Maybe session.onConnectionClosed() that calls session._state.onConnectionClosed()
// Maybe this.setState(new ConnectionClosedState());
// Maybe both ??
console.log(`Player ${session.player.username} disconnected`);
});
// ____ ___ _ _ _ _ _____ ____ _____ _____ ____
// / ___/ _ \| \ | | \ | | ____/ ___|_ _| ____| _ \
// | | | | | | \| | \| | _|| | | | | _| | | | |
// | |__| |_| | |\ | |\ | |__| |___ | | | |___| |_| |
// \____\___/|_| \_|_| \_|_____\____| |_| |_____|____/
//------------------------------------------------------
// Handle New Socket Connections
//------------------------------
/** @param {WebSocket} websocket */
onConnectionEstabished(websocket) {
console.log("New connection established");
const session = new Session(websocket, this.game);
session.sendSystemMessage("dev", true);
// __ __ _____ ____ ____ _ ____ _____
// | \/ | ____/ ___/ ___| / \ / ___| ____|
// | |\/| | _| \___ \___ \ / _ \| | _| _|
// | | | | |___ ___) |__) / ___ \ |_| | |___
// |_| |_|_____|____/____/_/ \_\____|_____|
//--------------------------------------------
// HANDLE INCOMING MESSAGES
//-------------------------
websocket.on("message", (data) => {
try {
console.debug("incoming websocket message %s", data);
// ____ _ ___ ____ _____
// / ___| | / _ \/ ___|| ____|
// | | | | | | | \___ \| _|
// | |___| |__| |_| |___) | |___
// \____|_____\___/|____/|_____|
//-------------------------------
// Handle Socket Closing
//----------------------
websocket.on("close", () => {
if (!session.player) {
console.info("A player without a session disconnected");
return;
}
//-------------
// TODO
//-------------
// Handle player logout (move the or hide their characters)
//
// Maybe session.onConnectionClosed() that calls session._state.onConnectionClosed()
// Maybe this.setState(new ConnectionClosedState());
// Maybe both ??
console.log(`Player ${session.player.username} disconnected`);
});
if (!session.state) {
console.error(
"we received a message, but don't even have a state. Zark!",
);
websocket.send(
msg.prepare(msg.ERROR, "Oh no! I don't know what to do!?"),
);
return;
}
// __ __ _____ ____ ____ _ ____ _____
// | \/ | ____/ ___/ ___| / \ / ___| ____|
// | |\/| | _| \___ \___ \ / _ \| | _| _|
// | | | | |___ ___) |__) / ___ \ |_| | |___
// |_| |_|_____|____/____/_/ \_\____|_____|
//--------------------------------------------
// HANDLE INCOMING MESSAGES
//-------------------------
websocket.on("message", (data) => {
try {
console.debug("incoming websocket message %s", data);
const msgObj = new msg.ClientMessage(data.toString());
if (!session.state) {
console.error("we received a message, but don't even have a state. Zark!");
websocket.send(msg.prepare(msg.ERROR, "Oh no! I don't know what to do!?"));
return;
}
if (msgObj.isQuitCommand()) {
//---------------------
// TODO TODO TODO TODO
//---------------------
// Set state = QuitState
//
websocket.send(
msg.prepare(
msg.MESSAGE,
"The quitting quitter quits... Typical. Cya!",
),
);
websocket.close();
return;
}
const msgObj = new msg.ClientMessage(data.toString());
if (typeof session.state.onMessage !== "function") {
console.error(
"we received a message, but we're not i a State to receive it",
);
websocket.send(
msg.prepare(
msg.ERROR,
"Oh no! I don't know what to do with that message.",
),
);
return;
}
session.state.onMessage(msgObj);
} catch (error) {
console.trace(
"received an invalid message (error: %s)",
error,
data.toString(),
data,
);
websocket.send(msg.prepare(msg.CALAMITY, error));
}
});
if (msgObj.isQuitCommand()) {
//---------------------
// TODO TODO TODO TODO
//---------------------
// Set state = QuitState
//
websocket.send(msg.prepare(msg.MESSAGE, "The quitting quitter quits... Typical. Cya!"));
websocket.close();
return;
}
session.setState(new AuthState(session));
}
if (typeof session.state.onMessage !== "function") {
console.error("we received a message, but we're not i a State to receive it");
websocket.send(msg.prepare(msg.ERROR, "Oh no! I don't know what to do with that message."));
return;
}
session.state.onMessage(msgObj);
} catch (error) {
console.trace("received an invalid message (error: %s)", error, data.toString(), data);
websocket.send(msg.prepare(msg.CALAMITY, error));
}
});
// ____ _____ _ ____ _____
// / ___|_ _|/ \ | _ \_ _|
// \___ \ | | / _ \ | |_) || |
// ___) || |/ ___ \| _ < | |
// |____/ |_/_/ \_\_| \_\|_|
//-----------------------------
// Start the server
//-----------------
start() {
//
// The file types we allow to be served.
const contentTypes = {
".js": "application/javascript",
".css": "text/css",
".html": "text/html",
};
session.setState(new AuthState(session));
}
//
// Create HTTP server for serving the client - Consider moving to own file
const httpServer = http.createServer((req, res) => {
let filePath = path.join(
"public",
req.url === "/" ? "index.html" : req.url,
);
const ext = path.extname(filePath);
const contentType = contentTypes[ext];
// ____ _____ _ ____ _____
// / ___|_ _|/ \ | _ \_ _|
// \___ \ | | / _ \ | |_) || |
// ___) || |/ ___ \| _ < | |
// |____/ |_/_/ \_\_| \_\|_|
//-----------------------------
// Start the server
//-----------------
start() {
//
// The file types we allow to be served.
const contentTypes = {
".js": "application/javascript",
".css": "text/css",
".html": "text/html",
};
//
// Check if the requested file has a legal file type.
if (!contentType) {
// Invalid file, pretend it did not exist!
res.writeHead(404);
res.end(`File not found`);
console.log("Bad http request", req.url);
return;
}
//
// Create HTTP server for serving the client - Consider moving to own file
const httpServer = http.createServer((req, res) => {
let filePath = path.join("public", req.url === "/" ? "index.html" : req.url);
const ext = path.extname(filePath);
const contentType = contentTypes[ext];
//
// Check if the file exists.
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404);
res.end(`File not found`);
console.log("Bad http request", req.url);
return;
}
res.writeHead(200, { "Content-Type": contentType });
res.end(data);
});
});
//
// Check if the requested file has a legal file type.
if (!contentType) {
// Invalid file, pretend it did not exist!
res.writeHead(404);
res.end(`File not found`);
console.log("Bad http request", req.url);
return;
}
//
// Create WebSocket server
const websocketServer = new WebSocketServer({ server: httpServer });
//
// Check if the file exists.
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404);
res.end(`File not found`);
console.log("Bad http request", req.url);
return;
}
res.writeHead(200, { "Content-Type": contentType });
res.end(data);
});
});
websocketServer.on("connection", (ws) => {
this.onConnectionEstabished(ws);
});
//
// Create WebSocket server
const websocketServer = new WebSocketServer({ server: httpServer });
console.info(`running environment: ${Config.env}`);
httpServer.listen(Config.port, () => {
console.log(`NUUHD server running on port ${Config.port}`);
console.log(`WebSocket server ready for connections`);
});
}
websocketServer.on("connection", (ws) => {
this.onConnectionEstabished(ws);
});
console.info(`running environment: ${Config.env}`);
httpServer.listen(Config.port, () => {
console.log(`NUUHD server running on port ${Config.port}`);
console.log(`WebSocket server ready for connections`);
});
}
}
// __ __ _ ___ _ _
// | \/ | / \ |_ _| \ | |
// | |\/| | / _ \ | || \| |
// | | | |/ ___ \ | || |\ |
// |_| |_/_/ \_\___|_| \_|
// |_| |_/_/ \_\___|_| \_| A
//---------------------------
// Code entry point
//-----------------
const mudserver = new MudServer();
const mudserver = new MudServer(/* location of crypto key for saving games */);
mudserver.start();

View File

@@ -4,32 +4,32 @@ import { AwaitCommandsState } from "./awaitCommands.js";
/** @interface */
export class JustLoggedInState {
/** @param {Session} session */
constructor(session) {
/** @type {Session} */
this.session = session;
}
// Show welcome screen
onAttach() {
this.session.sendMessage([
"",
"Welcome",
"",
"You can type “:quit” at any time to quit the game",
"",
]);
//
// Check if we need to create characters for the player
if (this.session.player.characters.size === 0) {
this.session.sendMessage(
"You haven't got any characters, so let's make some\n\n",
);
this.session.setState(new PartyCreationState(this.session));
return;
/** @param {Session} session */
constructor(session) {
/** @type {Session} */
this.session = session;
}
this.session.setState(new AwaitCommandsState(this.session));
}
// Show welcome screen
onAttach() {
this.session.sendMessage(["", "Welcome", "", "You can type “:quit” at any time to quit the game", ""]);
//
// Check if we need to create characters for the player
if (this.session.player.characters.size === 0) {
this.session.sendMessage("You haven't got any characters, so let's make some\n\n");
this.session.setState(new PartyCreationState(this.session));
return;
}
const replacer = (key, value) => {
if (value instanceof Set) {
return [...value]; // turn Set into array
}
return value;
}
this.session.sendMessage(JSON.stringify(this.session.player.characters.entries(), replacer, "\t"));
this.session.setState(new AwaitCommandsState(this.session));
}
}

View File

@@ -1,191 +0,0 @@
Here are some ASCII and UTF-8 characters commonly used for "shading" effects in text art or terminal displays. These characters provide varying levels of density or shading:
### ASCII Shading Characters
These are basic ASCII characters often used for shading:
```
Light shade: ░ (U+2591)
Medium shade: ▒ (U+2592)
Dark shade: ▓ (U+2593)
Full block: █ (U+2588)
Half block: ▄ (U+2584), ▀ (U+2580)
```
### Additional UTF-8 Block Characters
These Unicode characters offer more granular shading or block patterns:
```
Light block: ░ (U+2591)
Medium block: ▒ (U+2592)
Dark block: ▓ (U+2593)
Solid block: █ (U+2588)
Upper half block: ▀ (U+2580)
Lower half block: ▄ (U+2584)
Left half block: ▌ (U+258C)
Right half block: ▐ (U+2590)
Quadrant blocks: ▖ (U+2596), ▗ (U+2597), ▘ (U+2598), ▝ (U+259D)
Checkerboard: ▚ (U+259A), ▞ (U+259E)
```
### Example Usage
Heres an example of a simple shading gradient using some of these characters:
```
Light to Dark: ░ ▒ ▓ █
Half blocks: ▀ ▄ ▌ ▐
Quadrant pattern: ▖ ▗ ▘ ▝
```
### Notes
- Not all terminals or text editors display Unicode characters consistently, so ASCII characters like `.:;#+` are sometimes used for basic shading in simpler environments.
- If you want to create specific patterns or need more complex ASCII art, let me know, and I can generate or suggest more detailed designs!
- If you meant something specific by "shading" (e.g., for a particular programming context or art style), please clarify, and Ill tailor the response further.
Below is a collection of ASCII and UTF-8 characters suitable for creating frames, borders, or "windows" in a text-based user interface (TUI), such as those built with ncurses. These characters can be used to draw boxes, lines, and corners to simulate window-like structures in a terminal.
### ASCII Characters for Frames
These are basic ASCII characters that work universally in most terminals:
```
Horizontal line: - (U+002D)
Vertical line: | (U+007C)
Cross/intersection: + (U+002B)
Corners:
Top-left: + (U+002B) or `
Top-right: + (U+002B) or '
Bottom-left: + (U+002B) or ,
Bottom-right: + (U+002B) or .
```
Example simple ASCII window:
```
+----------+
| Content |
+----------+
```
### UTF-8 Box-Drawing Characters
Unicode provides a dedicated **Box Drawing** block (U+2500U+257F) for creating more refined frames. These are widely supported in modern terminals and ncurses:
#### Single-Line Box Drawing
```
Horizontal line: ─ (U+2500)
Vertical line: │ (U+2502)
Corners:
Top-left: ┌ (U+250C)
Top-right: ┐ (U+2510)
Bottom-left: └ (U+2514)
Bottom-right: ┘ (U+2518)
Intersections:
T-junction top: ┬ (U+252C)
T-junction bottom: ┴ (U+2534)
T-junction left: ├ (U+251C)
T-junction right: ┤ (U+2524)
Cross: ┼ (U+253C)
```
Example single-line window:
```
┌──────────┐
│ Content │
└──────────┘
```
#### Double-Line Box Drawing
```
Horizontal line: ═ (U+2550)
Vertical line: ║ (U+2551)
Corners:
Top-left: ╔ (U+2554)
Top-right: ╗ (U+2557)
Bottom-left: ╚ (U+255A)
Bottom-right: ╝ (U+255D)
Intersections:
T-junction top: ╦ (U+2566)
T-junction bottom: ╩ (U+2569)
T-junction left: ╠ (U+2560)
T-junction right: ╣ (U+2563)
Cross: ╬ (U+256C)
```
Example double-line window:
```
╔══════════╗
║ Content ║
╚══════════╝
```
#### Mixed and Other Box-Drawing Characters
For more complex designs, you can mix single and double lines or use specialized characters:
```
Single to double transitions:
Horizontal single to double: ╼ (U+257C)
Vertical single to double: ╽ (U+257D)
Rounded corners (less common, not always supported):
Top-left: ╭ (U+256D)
Top-right: ╮ (U+256E)
Bottom-left: ╰ (U+2570)
Bottom-right: ╯ (U+256F)
```
Example with rounded corners:
```
╭──────────╮
│ Content │
╰──────────╯
```
### Additional UTF-8 Characters for Decoration
These can enhance the appearance of your TUI:
```
Block elements for borders or shading:
Full block: █ (U+2588)
Half blocks: ▀ (U+2580), ▄ (U+2584), ▌ (U+258C), ▐ (U+2590)
Light shade for background: ░ (U+2591)
Medium shade: ▒ (U+2592)
Dark shade: ▓ (U+2593)
```
### Example TUI Window with Content
Heres a sample of a more complex window using single-line box-drawing characters:
```
┌────────────────────┐
│ My TUI Window │
├────────────────────┤
│ Item 1 [ OK ] │
│ Item 2 [Cancel] │
└────────────────────┘
```
### Notes for ncurses
- **ncurses Compatibility**: ncurses supports both ASCII and UTF-8 box-drawing characters, but you must ensure the terminal supports Unicode (e.g., `LANG=en_US.UTF-8` environment variable). Use `initscr()` and `start_color()` in ncurses to handle rendering.
- **Terminal Support**: Some older terminals may not render UTF-8 characters correctly. Test your TUI in the target environment (e.g., xterm, gnome-terminal, or Alacritty).
- **Fallback**: If Unicode support is unreliable, stick to ASCII (`-`, `|`, `+`) for maximum compatibility.
- **ncurses Functions**: Use `box()` in ncurses to draw a border around a window automatically, or manually print characters with `mvaddch()` for custom designs.
### Tips
- Combine single and double lines for visual hierarchy (e.g., double lines for outer windows, single lines for inner sections).
- If you need specific examples (e.g., a multi-window layout or a dialog box), let me know, and I can provide a detailed ASCII/UTF-8 mockup or even pseudocode for ncurses.
- If you want a particular style (e.g., heavy lines, dashed lines, or specific layouts), please clarify, and Ill tailor the response.
Let me know if you need help implementing this in ncurses or want more specific frame designs!

View File

@@ -1,6 +1,14 @@
const UID_DIGITS = 12;
const MINI_UID_REGEX = /\.uid\.[a-z0-9]{6,}$/;
const ID_SANITY_REGEX = /^:([a-z0-9]+\.)*[a-z0-9_]+$/;
import * as regex from "./regex.js";
const MINI_UID_REGEX = regex.pretty(
"\.uid\.", // Mini-uids always begin with ".uid."
"[a-z0-9]{6,}$", // Terminated by 6 or more random numbers and lowercase letters.
);
const ID_SANITY_REGEX = regex.pretty(
"^:", // All ids start with a colon
"([a-z0-9]+\.)*?", // Middle -optional- part :myid.gogle.thing.thang.thong
"[a-z0-9_]+$", // The terminating part of the id is numbers, lowercase letters, and -notably- underscores.
);
/**
* Sanity check a string to see if it is a potential id.
@@ -17,17 +25,22 @@ export function isIdSane(id) {
return false;
}
return ID_SANITY_REGEX.test(id);
if (!ID_SANITY_REGEX.test(id)) {
return false;
}
return true;
}
/**
* @returns {string} crypto-unsafe pseudo random number.
* @returns {string} crypto-unsafe pseudo random numbe"r.
*
* Generate a random number, convert it to base36, and return it as a string with 7-8 characters.
*/
export function miniUid() {
// we use 12 digits, but we could go up to 16
return Number(Math.random().toFixed(UID_DIGITS).substring(2)).toString(36);
// we use 12 digits, but we could go all the way to 16
const digits = 12;
return Number(Math.random().toFixed(digits).substring(2)).toString(36);
}
/**

80
server/utils/random.js Executable file
View File

@@ -0,0 +1,80 @@
/**
* Pseudo random number generator
* using the xorshift32 method.
*/
export class Xorshift32 {
/* @type {number} */
state;
/** @param {number} seed */
constructor(seed) {
this.state = seed | 0;
}
/** @protected Shuffle the internal state. */
shuffle() {
//
// Run the actual xorshift32 algorithm
let x = this.state;
x ^= x << 13;
x ^= x >>> 17;
x ^= x << 5;
x = (x >>> 0) / 4294967296;
this.state = x;
}
/**
* Get a random number and shuffle the internal state.
* @returns {number} a pseudo-random positive integer.
*/
get() {
this.shuffle();
return this.state;
}
/** @param {number} x @returns {number} a positive integer lower than x */
lowerThan(x) {
return this.get() % x;
}
/** @param {number} x @reurns {number} a positive integer lower than or equal to x */
lowerThanOrEqual(x) {
return this.get() % (x + 1);
}
/**
* @param {<T>[]} arr
*
* @return {<T>}
*/
randomElement(arr) {
const idx = this.lowerThan(arr.length);
return arr[idx];
}
/**
* @param {...<T>} args
* @returns {<T>}
*/
oneOf(...args) {
const idx = this.lowerThan(args.length);
return args[idx];
}
/**
* @param {number} lowerThanOrEqual a positive integer
* @param {number} greaterThanOrEqual a positive integer greater than lowerThanOrEqual
* @returns {number} a pseudo-random integer
*/
within(greaterThanOrEqual, lowerThanOrEqual) {
const range = lowerThanOrEqual - greaterThanOrEqual;
const num = this.lowerThanOrEqual(range);
return num + greaterThanOrEqual;
}
}

10
server/utils/regex.js Normal file
View File

@@ -0,0 +1,10 @@
/**
* Makes it easier to document regexes because you can break them up
*
* @param {...string} args
* @returns {Regexp}
*/
export function pretty(...args) {
const regexprStr = args.join("");
return new RegExp(regexprStr);
}