mirror of
https://github.com/FullScreenShenanigans/EightBittr.git
synced 2026-08-12 02:18:15 -07:00
feat: revamped WorldSeedr (#341)
## Overview Drastically simplifies the WorldSeedr package by reducing the different kinds of variance allowed. Adds unit tests too. Creates a second example, InfiniteContributionsCalendar, to demonstrate WorldSeedr usage. ### PR Checklist - [x] Fixes #182 - [x] I have run this code to verify it works - [x] This PR includes unit tests for the code change
This commit is contained in:
+4
-1
@@ -29,7 +29,10 @@
|
|||||||
],
|
],
|
||||||
|
|
||||||
// Off-by-default rules we want on
|
// Off-by-default rules we want on
|
||||||
"simple-import-sort/imports": "error"
|
"simple-import-sort/imports": "error",
|
||||||
|
|
||||||
|
// On-by-default rules we want off
|
||||||
|
"no-case-declarations": "off"
|
||||||
},
|
},
|
||||||
"root": true
|
"root": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
dist/
|
||||||
|
lib/
|
||||||
|
test/
|
||||||
|
webpack.config.js
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*.tsbuildinfo
|
||||||
|
node_modules/
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<!-- Top -->
|
||||||
|
|
||||||
|
# InfiniteContributionsCalendar
|
||||||
|
|
||||||
|
[](https://prettier.io)
|
||||||
|

|
||||||
|
[](http://badge.fury.io/js/infinite-contributions-calendar)
|
||||||
|
[](https://gitter.im/FullScreenShenanigans/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
|
||||||
|
Generates an infinite GitHub-like contributions calendar using EightBittr and WorldSeedr.
|
||||||
|
|
||||||
|
<!-- /Top -->
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Documentation coming soon™️!
|
||||||
|
|
||||||
|
<!-- Development -->
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
This repository is a portion of the [EightBittr monorepo](https://raw.githubusercontent.com/FullScreenShenanigans/EightBittr).
|
||||||
|
See its [docs/Development.md](../../docs/Development.md) for details on how to get started. 💖
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```shell
|
||||||
|
yarn run test
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests are written in [Mocha](https://github.com/mochajs/mocha) and [Chai](https://github.com/chaijs/chai).
|
||||||
|
Their files are written using alongside source files under `src/` and named `*.test.ts?`.
|
||||||
|
Whenever you add, remove, or rename a `*.test.t*` file under `src/`, `watch` will re-run `yarn run test:setup` to regenerate the list of static test files in `test/index.html`.
|
||||||
|
You can open that file in a browser to debug through the tests, or run `yarn test:run` to run them in headless Chrome.
|
||||||
|
|
||||||
|
<!-- Maps -->
|
||||||
|
<!-- /Maps -->
|
||||||
|
|
||||||
|
<!-- /Development -->
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"browser": "./lib/index.js",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/FullScreenShenanigans/EightBittr/issues"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"eightbittr": "^0.8.8",
|
||||||
|
"userwrappr": "^0.8.8",
|
||||||
|
"worldseedr": "^0.8.8"
|
||||||
|
},
|
||||||
|
"description": "Generates an infinite GitHub-like contributions calendar using EightBittr and WorldSeedr.",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/chai": "^4.3.3",
|
||||||
|
"@types/mocha": "^9.1.1",
|
||||||
|
"@types/sinon": "^10.0.13",
|
||||||
|
"@types/sinon-chai": "^3.2.8",
|
||||||
|
"chai": "^4.3.6",
|
||||||
|
"husky": "^8.0.1",
|
||||||
|
"lint-staged": "^13.0.3",
|
||||||
|
"mocha": "^10.0.0",
|
||||||
|
"mocha-headless-chrome": "^4.0.0",
|
||||||
|
"preact": "^10.10.2",
|
||||||
|
"requirejs": "^2.3.6",
|
||||||
|
"shenanigans-manager": "^0.8.8",
|
||||||
|
"sinon": "^14.0.0",
|
||||||
|
"sinon-chai": "^3.7.0",
|
||||||
|
"ts-loader": "^9.3.1",
|
||||||
|
"webpack": "^5.74.0",
|
||||||
|
"webpack-cli": "^4.10.0"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "lint-staged"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"lint-staged": {
|
||||||
|
"**/*.{css,js,json,md,ts,tsx}": [
|
||||||
|
"prettier --ignore-path .prettierignore --write"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "infinite-contributions-calendar",
|
||||||
|
"package": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "ssh://git@github.com:FullScreenShenanigans/EightBittr.git"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rm -rf dist lib *.tsbuildinfo",
|
||||||
|
"compile": "tsc -b",
|
||||||
|
"dist": "webpack",
|
||||||
|
"format": "prettier --ignore-path .prettierignore --check \"./**/*.{css,js,json,md,ts,tsx}\"",
|
||||||
|
"format:write": "yarn format --write",
|
||||||
|
"hydrate": "yarn shenanigans-manager hydrate",
|
||||||
|
"link": "yarn link",
|
||||||
|
"lint": "eslint --ignore-path .eslintignore \"./src/**/*.{ts,tsx}\"",
|
||||||
|
"prepare": "husky install",
|
||||||
|
"publish:ci": "yarn shenanigans-manager publish-if-updated",
|
||||||
|
"test": "yarn run test:setup && yarn run test:run",
|
||||||
|
"test:run": "yarn mocha-headless-chrome --file test/index.html --polling 1000",
|
||||||
|
"test:setup": "yarn shenanigans-manager generate-tests"
|
||||||
|
},
|
||||||
|
"shenanigans": {
|
||||||
|
"dist": true,
|
||||||
|
"example": true,
|
||||||
|
"game": true,
|
||||||
|
"loading": {
|
||||||
|
"externals": [
|
||||||
|
{
|
||||||
|
"js": {
|
||||||
|
"dev": "preact/dist/preact.umd",
|
||||||
|
"prod": "preact/dist/preact.umd"
|
||||||
|
},
|
||||||
|
"name": "preact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"js": {
|
||||||
|
"dev": "preact/hooks/dist/hooks.umd",
|
||||||
|
"prod": "preact/hooks/dist/hooks.umd"
|
||||||
|
},
|
||||||
|
"name": "preact/hooks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"js": {
|
||||||
|
"dev": "preact/jsx-runtime/dist/jsxRuntime.umd",
|
||||||
|
"prod": "preact/jsx-runtime/dist/jsxRuntime.umd"
|
||||||
|
},
|
||||||
|
"name": "preact/jsx-runtime"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"js": {
|
||||||
|
"dev": "userwrappr/dist/UserWrappr-Delayed",
|
||||||
|
"prod": "userwrappr/dist/UserWrappr-Delayed"
|
||||||
|
},
|
||||||
|
"name": "UserWrappr-Delayed",
|
||||||
|
"shenanigansPackage": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "InfiniteContributionsCalendar",
|
||||||
|
"web": true
|
||||||
|
},
|
||||||
|
"types": "./lib/index.d.ts",
|
||||||
|
"version": "0.8.14"
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { factory, member } from "autofieldr";
|
||||||
|
import { Actor, EightBittr, EightBittrConstructorSettings } from "eightbittr";
|
||||||
|
import { GroupHoldr } from "groupholdr";
|
||||||
|
import { WorldSeedr } from "worldseedr";
|
||||||
|
|
||||||
|
import { createWorldSeeder } from "./creators/createWorldSeeder";
|
||||||
|
import { Graphics } from "./sections/Graphics";
|
||||||
|
import { Groups } from "./sections/Groups";
|
||||||
|
import { Objects } from "./sections/Objects";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an infinite GitHub-like contributions calendar using EightBittr and WorldSeedr.
|
||||||
|
*/
|
||||||
|
export class InfiniteContributionsCalendar extends EightBittr {
|
||||||
|
/**
|
||||||
|
* Stores the visual appearance of Actors.
|
||||||
|
*/
|
||||||
|
@member(Graphics)
|
||||||
|
public readonly graphics: Graphics<this>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection settings for Actor group names.
|
||||||
|
*/
|
||||||
|
@member(Groups)
|
||||||
|
public readonly groups: Groups<this>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General storage abstraction for keyed containers of items.
|
||||||
|
*/
|
||||||
|
public readonly groupHolder: GroupHoldr<{
|
||||||
|
Squares: Actor;
|
||||||
|
Players: Actor;
|
||||||
|
Text: Actor;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw ObjectMakr factory settings.
|
||||||
|
*/
|
||||||
|
@member(Objects)
|
||||||
|
public readonly objects: Objects<this>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema-driven pseudorandom recursive generation of possibilities.
|
||||||
|
*/
|
||||||
|
@factory(createWorldSeeder)
|
||||||
|
public readonly worldSeeder: WorldSeedr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new instance of the InfiniteContributionsCalendar class.
|
||||||
|
*
|
||||||
|
* @param settings Settings to be used for initialization.
|
||||||
|
*/
|
||||||
|
public constructor(settings: EightBittrConstructorSettings) {
|
||||||
|
super(settings);
|
||||||
|
|
||||||
|
this.quadsKeeper.resetQuadrants();
|
||||||
|
|
||||||
|
const height = roundUpTo(settings.height, 12);
|
||||||
|
const width = roundUpTo(settings.width, 12);
|
||||||
|
|
||||||
|
const results = this.worldSeeder.generate("Area", {
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: width,
|
||||||
|
top: height,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
this.actors.add(result.title, result.area.left, height - result.area.top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function roundUpTo(amount: number, rounder: number) {
|
||||||
|
return Math.ceil(amount / rounder) * rounder;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { InfiniteContributionsCalendar } from "InfiniteContributionsCalendar";
|
||||||
|
import { WorldSeedr } from "worldseedr";
|
||||||
|
|
||||||
|
export const createWorldSeeder = (game: InfiniteContributionsCalendar) =>
|
||||||
|
new WorldSeedr({
|
||||||
|
possibilities: {
|
||||||
|
Area: {
|
||||||
|
children: {
|
||||||
|
size: {
|
||||||
|
height: game.settings.height,
|
||||||
|
width: 10,
|
||||||
|
},
|
||||||
|
title: "Column",
|
||||||
|
type: "Possibility",
|
||||||
|
},
|
||||||
|
direction: "right",
|
||||||
|
repeat: Infinity,
|
||||||
|
size: {
|
||||||
|
height: game.settings.height,
|
||||||
|
width: game.settings.width,
|
||||||
|
},
|
||||||
|
spacing: 2,
|
||||||
|
},
|
||||||
|
Column: {
|
||||||
|
children: {
|
||||||
|
size: {
|
||||||
|
height: 10,
|
||||||
|
width: 10,
|
||||||
|
},
|
||||||
|
title: ["SquareL0", "SquareL1", "SquareL2", "SquareL3", "SquareL4"].map(
|
||||||
|
(value) => ({
|
||||||
|
probability: 20,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
type: "Result",
|
||||||
|
},
|
||||||
|
direction: "bottom",
|
||||||
|
repeat: Infinity,
|
||||||
|
size: {
|
||||||
|
height: game.settings.height,
|
||||||
|
width: 10,
|
||||||
|
},
|
||||||
|
spacing: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { UserWrappr } from "userwrappr";
|
||||||
|
export * from "./InfiniteContributionsCalendar";
|
||||||
|
|
||||||
|
import { createUserWrapprSettings } from "./interface/InterfaceSettings";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a UserWrappr interface around a new InfiniteContributionsCalendar game.
|
||||||
|
*
|
||||||
|
* @param container HTML element to create within.
|
||||||
|
*/
|
||||||
|
export const createInfiniteContributionsCalendarInterface = async (
|
||||||
|
container: HTMLElement
|
||||||
|
): Promise<void> => {
|
||||||
|
return new UserWrappr(createUserWrapprSettings()).createDisplay(container);
|
||||||
|
};
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { GameWindow } from "eightbittr";
|
||||||
|
import { AbsoluteSizeSchema, OptionType, UserWrapprSettings } from "userwrappr";
|
||||||
|
|
||||||
|
import { InfiniteContributionsCalendar } from "../InfiniteContributionsCalendar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global scope around a game, such as a DOM window.
|
||||||
|
*/
|
||||||
|
export interface WrappingGameWindow extends GameWindow {
|
||||||
|
/**
|
||||||
|
* Game instance, once this has created it.
|
||||||
|
*/
|
||||||
|
FSS?: InfiniteContributionsCalendar;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InterfaceSettingOverrides {
|
||||||
|
createGame?(size: AbsoluteSizeSchema): InfiniteContributionsCalendar;
|
||||||
|
gameWindow?: WrappingGameWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Friendly name of the default game size.
|
||||||
|
*/
|
||||||
|
const defaultSize = "Full";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sizes the game is allowed to be, keyed by friendly name.
|
||||||
|
*/
|
||||||
|
const sizes = {
|
||||||
|
[defaultSize]: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates settings for an UserWrappr that will create and wrap a InfiniteContributionsCalendar instance.
|
||||||
|
*
|
||||||
|
* @param gameWindow Global scope around the game interface, if not the global window.
|
||||||
|
*/
|
||||||
|
export const createUserWrapprSettings = ({
|
||||||
|
createGame = (size: AbsoluteSizeSchema) => new InfiniteContributionsCalendar(size),
|
||||||
|
gameWindow = window,
|
||||||
|
}: InterfaceSettingOverrides = {}): UserWrapprSettings => {
|
||||||
|
/**
|
||||||
|
* Game instance, once this has created it.
|
||||||
|
*/
|
||||||
|
let game: InfiniteContributionsCalendar;
|
||||||
|
|
||||||
|
return {
|
||||||
|
defaultSize: sizes[defaultSize],
|
||||||
|
createContents: (size: AbsoluteSizeSchema) => {
|
||||||
|
gameWindow.FSS = game = createGame(size);
|
||||||
|
game.inputs.initializeGlobalPipes(gameWindow);
|
||||||
|
game.frameTicker.play();
|
||||||
|
|
||||||
|
return game.container;
|
||||||
|
},
|
||||||
|
menus: [
|
||||||
|
{
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
action: (): void => {
|
||||||
|
game.utilities.takeScreenshot(
|
||||||
|
`InfiniteContributionsCalendar ${Date.now()}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
title: "Screenshot",
|
||||||
|
type: OptionType.Action,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
title: "Options",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
styles: {
|
||||||
|
input: {
|
||||||
|
fontFamily: "Press Start",
|
||||||
|
minWidth: "117px",
|
||||||
|
padding: "3px",
|
||||||
|
},
|
||||||
|
inputButton: {
|
||||||
|
background: "#ffcc33",
|
||||||
|
fontFamily: "Press Start",
|
||||||
|
padding: "7px 3px",
|
||||||
|
},
|
||||||
|
inputButtonAction: {
|
||||||
|
padding: "11px 3px",
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
inputButtonBoolean: {
|
||||||
|
padding: "7px 21px",
|
||||||
|
},
|
||||||
|
inputButtonOff: {
|
||||||
|
background: "#ccaa33",
|
||||||
|
},
|
||||||
|
inputSelect: {
|
||||||
|
minWidth: "35px",
|
||||||
|
padding: "3px 0",
|
||||||
|
},
|
||||||
|
option: {
|
||||||
|
alignItems: "center",
|
||||||
|
margin: "auto",
|
||||||
|
padding: "7px 0",
|
||||||
|
maxWidth: "calc(100% - 14px)",
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
left: "4px",
|
||||||
|
right: "4px",
|
||||||
|
width: "auto",
|
||||||
|
padding: "4px 3px 21px",
|
||||||
|
boxShadow: ["0 3px 7px black inset", "0 0 0 4px #99ccff", "0 0 14px black"].join(
|
||||||
|
", "
|
||||||
|
),
|
||||||
|
background: "#005599",
|
||||||
|
},
|
||||||
|
optionsList: {
|
||||||
|
margin: "0 0 21px",
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
maxWidth: "385px",
|
||||||
|
minWidth: "280px",
|
||||||
|
padding: "7px",
|
||||||
|
zIndex: "1",
|
||||||
|
},
|
||||||
|
menusInnerArea: {
|
||||||
|
background: "black",
|
||||||
|
color: "white",
|
||||||
|
fontFamily: "Press Start",
|
||||||
|
transition: "700ms color",
|
||||||
|
},
|
||||||
|
menuTitle: {
|
||||||
|
fontSize: "16px",
|
||||||
|
},
|
||||||
|
menuTitleButton: {
|
||||||
|
alignItems: "center",
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
color: "white",
|
||||||
|
display: "flex",
|
||||||
|
fontFamily: "Press Start",
|
||||||
|
fontSize: "16px",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
menuTitleButtonFake: {
|
||||||
|
color: "grey",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createInfiniteContributionsCalendarInterface } from "./index";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const container = document.getElementById("game")!;
|
||||||
|
|
||||||
|
createInfiniteContributionsCalendarInterface(container).catch((error: Error): void => {
|
||||||
|
console.error("An error happened while trying to instantiate InfiniteContributionsCalendar!");
|
||||||
|
console.error("requirejs error:", error);
|
||||||
|
});
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Graphics as EightBittrGraphics } from "eightbittr";
|
||||||
|
import { Palette } from "pixelrendr";
|
||||||
|
|
||||||
|
import { InfiniteContributionsCalendar } from "../InfiniteContributionsCalendar";
|
||||||
|
|
||||||
|
export class Graphics<
|
||||||
|
Game extends InfiniteContributionsCalendar
|
||||||
|
> extends EightBittrGraphics<Game> {
|
||||||
|
public readonly background = "white";
|
||||||
|
|
||||||
|
public readonly library = {
|
||||||
|
SquareL0: "0x18,0x180,0x18,0",
|
||||||
|
SquareL1: "0x28,0x280,0x28,0",
|
||||||
|
SquareL2: "0x38,0x380,0x38,0",
|
||||||
|
SquareL3: "0x48,0x480,0x48,0",
|
||||||
|
SquareL4: "0x58,0x580,0x58,0",
|
||||||
|
};
|
||||||
|
|
||||||
|
public readonly paletteDefault: Palette = [
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[255, 255, 255, 255],
|
||||||
|
// --color-calendar-graph-day-L1-bg: #9be9a8;
|
||||||
|
[155, 233, 168, 255],
|
||||||
|
// --color-calendar-graph-day-L2-bg: #40c463;
|
||||||
|
[64, 196, 99, 255],
|
||||||
|
// --color-calendar-graph-day-L3-bg: #30a14e;
|
||||||
|
[48, 161, 78, 255],
|
||||||
|
// --color-calendar-graph-day-L4-bg: #216e39;
|
||||||
|
[33, 110, 57, 255],
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Groups as EightBittrGroups } from "eightbittr";
|
||||||
|
|
||||||
|
import { InfiniteContributionsCalendar } from "../InfiniteContributionsCalendar";
|
||||||
|
|
||||||
|
export class Groups<Game extends InfiniteContributionsCalendar> extends EightBittrGroups<Game> {
|
||||||
|
public readonly groupNames = ["Players", "Squares", "Text"];
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Objects as ObjectsBase } from "eightbittr";
|
||||||
|
import { ClassInheritance, ClassProperties } from "objectmakr";
|
||||||
|
|
||||||
|
import { InfiniteContributionsCalendar } from "../InfiniteContributionsCalendar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw ObjectMakr factory settings.
|
||||||
|
*/
|
||||||
|
export class Objects<Game extends InfiniteContributionsCalendar> extends ObjectsBase<Game> {
|
||||||
|
public readonly inheritance: ClassInheritance = {
|
||||||
|
Actor: {
|
||||||
|
Square: {
|
||||||
|
SquareL0: {},
|
||||||
|
SquareL1: {},
|
||||||
|
SquareL2: {},
|
||||||
|
SquareL3: {},
|
||||||
|
SquareL4: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public readonly onMake = "onMake";
|
||||||
|
|
||||||
|
public readonly properties: ClassProperties = {
|
||||||
|
Actor: {
|
||||||
|
onMake: this.game.actors.process.bind(this.game.actors),
|
||||||
|
},
|
||||||
|
Square: {
|
||||||
|
height: 10,
|
||||||
|
groupType: "Squares",
|
||||||
|
width: 10,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./src",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "preact",
|
||||||
|
"module": "amd",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"outDir": "./lib",
|
||||||
|
"pretty": true,
|
||||||
|
"rootDir": "./src",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"strictFunctionTypes": false,
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
|
"target": "ES2021"
|
||||||
|
},
|
||||||
|
"exclude": ["**/*.d.ts", "**/lib"],
|
||||||
|
"include": ["./src/**/*.ts", "./src/**/*.tsx"]
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { member } from "autofieldr";
|
import { member } from "autofieldr";
|
||||||
import { FilterContainer, Palette } from "pixelrendr";
|
import { FilterContainer, LibraryRaws, Palette } from "pixelrendr";
|
||||||
|
|
||||||
import { EightBittr } from "../EightBittr";
|
import { EightBittr } from "../EightBittr";
|
||||||
import { Actor } from "../types";
|
import { Actor } from "../types";
|
||||||
@@ -34,7 +34,7 @@ export class Graphics<Game extends EightBittr> extends Section<Game> {
|
|||||||
/**
|
/**
|
||||||
* A nested library of sprites to process.
|
* A nested library of sprites to process.
|
||||||
*/
|
*/
|
||||||
public readonly library?: any;
|
public readonly library?: LibraryRaws;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default palette of colors to use for sprites.
|
* The default palette of colors to use for sprites.
|
||||||
|
|||||||
@@ -146,41 +146,6 @@ export class NumberMakr {
|
|||||||
return this.seed;
|
return this.seed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns The length of the state vector.
|
|
||||||
*/
|
|
||||||
public getStateLength(): number {
|
|
||||||
return this.stateLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns The length of the state vector.
|
|
||||||
*/
|
|
||||||
public getStatePeriod(): number {
|
|
||||||
return this.statePeriod;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns The length of the state vector.
|
|
||||||
*/
|
|
||||||
public getMatrixA(): number {
|
|
||||||
return this.matrixA;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns The length of the state vector.
|
|
||||||
*/
|
|
||||||
public getMaskUpper(): number {
|
|
||||||
return this.maskUpper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns The length of the state vector.
|
|
||||||
*/
|
|
||||||
public getMaskLower(): number {
|
|
||||||
return this.maskLower;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes state from a new seed.
|
* Initializes state from a new seed.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -7,10 +7,409 @@
|
|||||||
[](http://badge.fury.io/js/worldseedr)
|
[](http://badge.fury.io/js/worldseedr)
|
||||||
[](https://gitter.im/FullScreenShenanigans/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
[](https://gitter.im/FullScreenShenanigans/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
|
||||||
Automates random, recursive generation of possibilities from JSON schemas.
|
Schema-driven pseudorandom recursive generation of possibilities.
|
||||||
|
|
||||||
<!-- /Top -->
|
<!-- /Top -->
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
WorldSeedr allows generating descriptions of objects in a two-dimensional space based on probabilities in input schemas.
|
||||||
|
Think random map generation for video games, particularly rogue-likes.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const worldSeeder = new WorldSeedr({
|
||||||
|
possibilities: {
|
||||||
|
Stage: {
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
probability: 80,
|
||||||
|
value: {
|
||||||
|
size: 3,
|
||||||
|
title: "GoodActor",
|
||||||
|
type: "Result",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
probability: 20,
|
||||||
|
value: {
|
||||||
|
size: 3,
|
||||||
|
title: "GreatActor",
|
||||||
|
type: "Result",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
direction: "right",
|
||||||
|
repeat: Infinity,
|
||||||
|
size: {
|
||||||
|
height: 8,
|
||||||
|
width: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
random: ((values) => {
|
||||||
|
let index = -1;
|
||||||
|
return () => values[(index = (index + 1) % values.length)];
|
||||||
|
})([0.1, 0.9]),
|
||||||
|
});
|
||||||
|
|
||||||
|
worldSeeder.generate("Stage", { bottom: 0, left: 0, top: 8, right: 12 });
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Randomized results may look something like:
|
||||||
|
[
|
||||||
|
{ area: { left: 0, right: 3, bottom: 0, top: 8 }, title: "GoodActor" },
|
||||||
|
{ area: { left: 3, right: 6, bottom: 0, top: 8 }, title: "GreatActor" },
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
WorldSeedr's constructor takes in an object that may have two properties:
|
||||||
|
|
||||||
|
- `possibilities` _(required)_: Object mapping names of possibilities to their details
|
||||||
|
- `random` _(optional)_: A replacement for `Math.random`
|
||||||
|
|
||||||
|
### `possibilities`
|
||||||
|
|
||||||
|
Each Possibility represents a set of instructions for placing children along a line.
|
||||||
|
Each of the following properties of the `possibilities` object may be provided either as a single value or as an array of objects with a `probability` number between 0 and 100 and `value`.
|
||||||
|
Probability arrays will be chosen from at random, with weighting based on the probability.
|
||||||
|
|
||||||
|
For example, the `children` in this `Obstacle` Possibility have an 60% change of placing nothing, a 30% chance of placing two `"Enemy"`s, and a 10% chance of placing a `"DifficultEnemy"`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new WorldSeedr({
|
||||||
|
possibilities: {
|
||||||
|
Obstacle: {
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
probability: 60,
|
||||||
|
value: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
probability: 30,
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
size: 2,
|
||||||
|
title: "Enemy",
|
||||||
|
type: "Result",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
size: 2,
|
||||||
|
title: "Enemy",
|
||||||
|
type: "Result",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
probability: 10,
|
||||||
|
value: {
|
||||||
|
size: 4,
|
||||||
|
title: "DifficultEnemy",
|
||||||
|
type: "Result",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
direction: "right",
|
||||||
|
size: {
|
||||||
|
height: 4,
|
||||||
|
width: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `children`
|
||||||
|
|
||||||
|
An ordered array of descendants to place in the output space.
|
||||||
|
These each have a `type` property equal to either:
|
||||||
|
|
||||||
|
- `"Possibility"`: Indicating to recurse on another Possibility
|
||||||
|
- `"Result"`: Indicating the child should be directly returned as results
|
||||||
|
|
||||||
|
Both types of children must have a `title` property indicating the name of their specified output.
|
||||||
|
|
||||||
|
##### Possibility Children
|
||||||
|
|
||||||
|
It's common for Possibilities to recursively include other Possibilities, or even themselves.
|
||||||
|
WorldSeedr will call `.generate` on the nested Possibility within the space allocated for the child.
|
||||||
|
|
||||||
|
For example, given this WorldSeedr that defines a `"Dungeon"` Possibility as having a 50-50 chance of either recursing into a `"HappySpace"` Possibility or an `"UnhappyPlace"` Possibility:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const worldSeeder = new WorldSeedr({
|
||||||
|
possibilities: {
|
||||||
|
Dungeon: {
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
probability: 50,
|
||||||
|
value: {
|
||||||
|
size: 10,
|
||||||
|
title: "HappySpace",
|
||||||
|
type: "Possibility",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
probability: 50,
|
||||||
|
value: {
|
||||||
|
size: 10,
|
||||||
|
title: "UnhappySpace",
|
||||||
|
type: "Possibility",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
direction: "right",
|
||||||
|
repeat: 2,
|
||||||
|
size: {
|
||||||
|
height: 10,
|
||||||
|
width: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HappySpace: {
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
probability: 70,
|
||||||
|
value: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
probability: 30,
|
||||||
|
value: {
|
||||||
|
size: 10,
|
||||||
|
title: "Item",
|
||||||
|
type: "Result",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
direction: "right",
|
||||||
|
size: {
|
||||||
|
height: 10,
|
||||||
|
width: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UnhappySpace: {
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
probability: 80,
|
||||||
|
value: {
|
||||||
|
size: 10,
|
||||||
|
title: "EasyEnemy",
|
||||||
|
type: "Result",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
probability: 20,
|
||||||
|
value: {
|
||||||
|
size: 10,
|
||||||
|
title: "HardEnemy",
|
||||||
|
type: "Result",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
direction: "right",
|
||||||
|
size: {
|
||||||
|
height: 10,
|
||||||
|
width: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
...generated output might look something like:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
[
|
||||||
|
{
|
||||||
|
// area: { ... }
|
||||||
|
title: "HardEnemy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// area: { ... }
|
||||||
|
title: "Item",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Result Children
|
||||||
|
|
||||||
|
Result children may optionally include a `properties` object to be added to the results
|
||||||
|
|
||||||
|
For example, given this `children.value` that only adds `properties` to one of its two results:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
[
|
||||||
|
{
|
||||||
|
// size: ...
|
||||||
|
title: "Scenery",
|
||||||
|
type: "Result",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// size: ...
|
||||||
|
properties: {
|
||||||
|
dialog: "Roar!",
|
||||||
|
},
|
||||||
|
title: "Enemy",
|
||||||
|
type: "Result",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
...generated output might look something like:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
[
|
||||||
|
{
|
||||||
|
// area: { ... },
|
||||||
|
title: "Scenery",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// area: { ... },
|
||||||
|
properties: {
|
||||||
|
dialog: "Roar!",
|
||||||
|
},
|
||||||
|
title: "Enemy",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that results children, like other properties, can come in three forms:
|
||||||
|
|
||||||
|
- Direct value: like `children: { ... }`
|
||||||
|
- Array of values: like `children: [ { ... }, { ... } ]`
|
||||||
|
- Array of value probabilities: like `children: [ { probability: 100, value: { ... }]`
|
||||||
|
|
||||||
|
#### `align`
|
||||||
|
|
||||||
|
Possibility children won't always take up all the space to their side (perpendicular to their direction).
|
||||||
|
By default, they align to `"stretch"`: taking up all available space to their side.
|
||||||
|
They can alternately be given an `align` property set to one of the directions to align to.
|
||||||
|
|
||||||
|
For example, given this Possibility snippet that aligns one result to the default `"stretch"`, one to `"bottom"`, and one to `"top"`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
size: {
|
||||||
|
height: 5,
|
||||||
|
width: 5,
|
||||||
|
},
|
||||||
|
title: "First",
|
||||||
|
type: "Result",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: "bottom",
|
||||||
|
size: {
|
||||||
|
height: 5,
|
||||||
|
width: 5,
|
||||||
|
},
|
||||||
|
title: "Second",
|
||||||
|
type: "Result",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: "top",
|
||||||
|
size: {
|
||||||
|
height: 5,
|
||||||
|
width: 5,
|
||||||
|
},
|
||||||
|
title: "Third",
|
||||||
|
type: "Result",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
direction: "right",
|
||||||
|
size: {
|
||||||
|
height: 15,
|
||||||
|
width: 15,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
...generated output in a 15x15 space might look like:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
[
|
||||||
|
{
|
||||||
|
area: {
|
||||||
|
left: 0,
|
||||||
|
right: 5,
|
||||||
|
bottom: 0,
|
||||||
|
top: 15,
|
||||||
|
},
|
||||||
|
title: "First",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
area: {
|
||||||
|
left: 5,
|
||||||
|
right: 10,
|
||||||
|
bottom: 0,
|
||||||
|
top: 15,
|
||||||
|
},
|
||||||
|
title: "Second",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
area: {
|
||||||
|
left: 10,
|
||||||
|
right: 15,
|
||||||
|
bottom: 0,
|
||||||
|
top: 15,
|
||||||
|
},
|
||||||
|
title: "Third",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `direction`
|
||||||
|
|
||||||
|
Possibility children are placed along one of the cardinal directions.
|
||||||
|
The `direction` property may be one of:
|
||||||
|
|
||||||
|
- `"bottom"`: Start at the top edge of the available space, and go downwards
|
||||||
|
- `"left"`: Start at the right edge of the available space, and go to the left
|
||||||
|
- `"right"`: Start at the left edge of the available space, and go to the right
|
||||||
|
- `"top"`: Start at the bottom edge of the available space, and go upwards
|
||||||
|
|
||||||
|
#### `repeat`
|
||||||
|
|
||||||
|
How many times the children should be repeated.
|
||||||
|
It defaults to `1`, for only placing the children value(s) once.
|
||||||
|
|
||||||
|
> Tip: use `Infinity` to continuously repeat children as many times as allowed.
|
||||||
|
|
||||||
|
#### `size`
|
||||||
|
|
||||||
|
How much space this takes up, as an object containing `height` and `width`.
|
||||||
|
|
||||||
|
#### `spacing`
|
||||||
|
|
||||||
|
How much space should be between children.
|
||||||
|
It defaults to `0`, for no space between them.
|
||||||
|
The `spacing` property may be one of:
|
||||||
|
|
||||||
|
- A number: always use much space between children
|
||||||
|
- An object describing a range of random numbers:
|
||||||
|
- `max`: Maximum amount for the spacing
|
||||||
|
- `min`: Minimum amount for the spacing
|
||||||
|
- `roundTo`: Number unit to round to, if not 1 (no rounding)
|
||||||
|
- An array of objects containing `probability` and `value` numbers, where the values are the objects describing a range of random numbers
|
||||||
|
|
||||||
|
### `random`
|
||||||
|
|
||||||
|
Applications that use deterministic seeds to generate maps generally need to use their own random number generator instead of `Math.random()`.
|
||||||
|
See [NumberMakr](../numbermakr/README.md) for the recommended EightBittr package.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const numberMaker = new NumberMakr();
|
||||||
|
|
||||||
|
const worldSeeder = new WorldSeedr({
|
||||||
|
possibilities: {
|
||||||
|
/* ... */
|
||||||
|
},
|
||||||
|
random: () => numberMaker.random(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `generate`
|
||||||
|
|
||||||
<!-- Development -->
|
<!-- Development -->
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/FullScreenShenanigans/EightBittr/issues"
|
"url": "https://github.com/FullScreenShenanigans/EightBittr/issues"
|
||||||
},
|
},
|
||||||
"description": "Automates random, recursive generation of possibilities from JSON schemas.",
|
"description": "Schema-driven pseudorandom recursive generation of possibilities.",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.3.3",
|
"@types/chai": "^4.3.3",
|
||||||
"@types/mocha": "^9.1.1",
|
"@types/mocha": "^9.1.1",
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
import { RandomChooser } from "./RandomChooser";
|
||||||
|
|
||||||
|
describe("RandomChooser", () => {
|
||||||
|
describe("chooseAmong", () => {
|
||||||
|
it("returns the only choice when there is only one", () => {
|
||||||
|
// Arrange
|
||||||
|
const randomChooser = new RandomChooser(() => 0.99);
|
||||||
|
const value = "val";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = randomChooser.chooseAmong([{ probability: 100, value }]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).to.be.equal(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the first choice when its probability is the first reached", () => {
|
||||||
|
// Arrange
|
||||||
|
const randomChooser = new RandomChooser(() => 0.5);
|
||||||
|
const value = "first";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = randomChooser.chooseAmong([
|
||||||
|
{ probability: 60, value },
|
||||||
|
{ probability: 40, value: "incorrect" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).to.be.equal(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the second choice when its probability is the first reached", () => {
|
||||||
|
// Arrange
|
||||||
|
const randomChooser = new RandomChooser(() => 0.5);
|
||||||
|
const value = "first";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = randomChooser.chooseAmong([
|
||||||
|
{ probability: 40, value: "incorrect" },
|
||||||
|
{ probability: 60, value },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).to.be.equal(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws an error when the goal probability is never reached", () => {
|
||||||
|
// Arrange
|
||||||
|
const randomChooser = new RandomChooser(() => 0.99);
|
||||||
|
const value = "val";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const act = () => randomChooser.chooseAmong([{ probability: 5, value }]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(act).to.throw("Choices only reached sum 5 out of goal 99.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("chooseFixedOrRandom", () => {
|
||||||
|
it("returns the value when given a value", () => {
|
||||||
|
// Arrange
|
||||||
|
const randomChooser = new RandomChooser(() => 0);
|
||||||
|
const value = "val";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = randomChooser.chooseFixedOrRandom(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).to.be.equal(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a choice value when given choices", () => {
|
||||||
|
// Arrange
|
||||||
|
const randomChooser = new RandomChooser(() => 0.5);
|
||||||
|
const value = "first";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = randomChooser.chooseFixedOrRandom([
|
||||||
|
{ probability: 40, value: "incorrect" },
|
||||||
|
{ probability: 60, value },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).to.be.equal(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("chooseFixedOrRandomOr", () => {
|
||||||
|
it("returns the default value when the value doesn't exist", () => {
|
||||||
|
// Arrange
|
||||||
|
const randomChooser = new RandomChooser(() => 0);
|
||||||
|
const value = "val";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = randomChooser.chooseFixedOrRandomOr(undefined, value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).to.be.equal(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the value when given a value", () => {
|
||||||
|
// Arrange
|
||||||
|
const randomChooser = new RandomChooser(() => 0);
|
||||||
|
const value = "val";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = randomChooser.chooseFixedOrRandomOr(value, "incorrect");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).to.be.equal(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { FixedOrRandomized, RandomizedValue, RandomNumberGenerator } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses values from arrays of weighted random values.
|
||||||
|
*/
|
||||||
|
export class RandomChooser {
|
||||||
|
public constructor(
|
||||||
|
/**
|
||||||
|
* Random number generator that returns a decimal within [0, 1).
|
||||||
|
*/
|
||||||
|
private readonly random: RandomNumberGenerator
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses a value from an array of weighted random values.
|
||||||
|
*
|
||||||
|
* @param choices Weighted choices to pick from.
|
||||||
|
* @returns Random value from the choices.
|
||||||
|
*/
|
||||||
|
public chooseAmong<Value>(choices: RandomizedValue<Value>[]) {
|
||||||
|
const goal = this.randomBetween(0, 100);
|
||||||
|
let sum = 0;
|
||||||
|
|
||||||
|
for (const choice of choices) {
|
||||||
|
sum += choice.probability;
|
||||||
|
if (sum >= goal) {
|
||||||
|
return choice.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Choices only reached sum ${sum} out of goal ${goal}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses a value from either just the value or an array of weighted random values.
|
||||||
|
*
|
||||||
|
* @param possibility The value or weighted choices to pick from.
|
||||||
|
* @returns Random value from the choices.
|
||||||
|
*/
|
||||||
|
public chooseFixedOrRandom<Value>(possibility: FixedOrRandomized<Value>) {
|
||||||
|
return isRandomizedPossibility<Value>(possibility)
|
||||||
|
? this.chooseAmong(possibility)
|
||||||
|
: possibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses a value from an array of weighted random values, or a default if no values exist.
|
||||||
|
*
|
||||||
|
* @param possibility Weighted choices to pick from.
|
||||||
|
* @returns Random value from the choices, or the default value.
|
||||||
|
*/
|
||||||
|
public chooseFixedOrRandomOr<Value>(
|
||||||
|
possibility: FixedOrRandomized<Value> | undefined,
|
||||||
|
defaultValue: Value
|
||||||
|
) {
|
||||||
|
return possibility === undefined ? defaultValue : this.chooseFixedOrRandom(possibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Random number generator that returns a decimal within [min, max).
|
||||||
|
*
|
||||||
|
* @param min Minimum number to return within.
|
||||||
|
* @param max Maximum number to return within.
|
||||||
|
* @returns Number within [min, max] at random.
|
||||||
|
*/
|
||||||
|
public randomBetween(min: number, max: number) {
|
||||||
|
return Math.floor(this.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRandomizedPossibility<Value>(
|
||||||
|
possibility: FixedOrRandomized<Value>
|
||||||
|
): possibility is RandomizedValue<Value>[] {
|
||||||
|
return Array.isArray(possibility) && "probability" in possibility[0];
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user