utils/ts_core
Krzysztof kuhy Rudnicki c5b3fceeae ts_core 0.1.1, web_ui 0.4.0: consume @kuhyx/ts-config
The two shared packages are the first repos on the preset (link:../ts_config
inside the monorepo; consumers outside use the git tag). Each eslint.config.js
is now the defineConfig() call plus one override with its reason: a public
API name (`nextChance`, `fuzzyMatch`) the boolean-prefix rule would rename,
which is a breaking change for the three repos importing it.

Everything else the preset flagged is fixed in code, not disabled:
key order (perfectionist), hex casing, sort compare functions in tests,
Iterator#map().toArray(), explicit returns on multiline arrows.

web_ui: react-refresh/only-export-components was a real finding --
range-slider.tsx exported `steppedValue` next to the component, which breaks
Vite HMR for the module. The keyboard contract moves to stepped-value.ts as
`steppedValue` + a new pure `keyTarget(key, values, current)`, which also
resolves unicorn/no-late-event-control: the handler now decides the target
first and calls preventDefault() only when it owns the key. Its tests move
with it (the 250-line gate caught the combined file). Minor bump for the new
export. dist/ rebuilt in both packages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnAYyNH1Sm4iJNidfYbrZJ
2026-09-16 21:46:41 +02:00
..
dist ts_core 0.1.1, web_ui 0.4.0: consume @kuhyx/ts-config 2026-09-16 21:46:41 +02:00
src ts_core 0.1.1, web_ui 0.4.0: consume @kuhyx/ts-config 2026-09-16 21:46:41 +02:00
.gitignore ts_core: a framework-free TypeScript utility package 2026-08-15 15:42:41 +02:00
eslint.config.js ts_core 0.1.1, web_ui 0.4.0: consume @kuhyx/ts-config 2026-09-16 21:46:41 +02:00
package.json ts_core 0.1.1, web_ui 0.4.0: consume @kuhyx/ts-config 2026-09-16 21:46:41 +02:00
pnpm-lock.yaml ts_core 0.1.1, web_ui 0.4.0: consume @kuhyx/ts-config 2026-09-16 21:46:41 +02:00
README.md ts_core: a framework-free TypeScript utility package 2026-08-15 15:42:41 +02:00
tsconfig.json ts_core 0.1.1, web_ui 0.4.0: consume @kuhyx/ts-config 2026-09-16 21:46:41 +02:00
tsconfig.lint.json ts_core: a framework-free TypeScript utility package 2026-08-15 15:42:41 +02:00
vitest.config.ts ts_core: a framework-free TypeScript utility package 2026-08-15 15:42:41 +02:00

@kuhyx/ts-core

Framework-free TypeScript utilities shared across kuhy's games and web apps.

Deliberately no React and no CSS — those live in @kuhyx/web-ui. A game that needs a seeded RNG should not have to install a UI package to get one; that coupling is what makes people vendor their own copy instead, which is how these ended up duplicated four ways.

Install

pnpm only. npm cannot install a subdirectory of a git repo — it ignores the &path: key, looks for package.json at the repo root and fails with ENOENT. Every consumer of this package must be on pnpm.

// package.json
"dependencies": {
  "@kuhyx/ts-core": "github:kuhyx/utils#ts_core-v0.1.0&path:/ts_core"
}

What it ships

Export Notes
Clock, createRealClock, realClock, createManualClock, ManualClock Injectable time source, so real-time mechanics are driven by a scripted clock in tests instead of racing the wall clock.
Rng, createRng, nextFloat, nextInt, nextChance, pick Seeded mulberry32, free-function style over mutable state.
SeededRng, createSeededRng The same core, closure style.

Why the RNG has two interfaces

The repos that had grown their own copy did not agree on shape: konbini-67 used mutable state plus free functions (nextFloat(rng)); sims3-clone and europe-county-map returned an object of closures (rng.next() / rng.float()).

Both ship here rather than being reconciled into one, because the sequence is the contract. These repos have seeded tests and "same seed → identical world" guarantees, so changing how many times a helper advances the generator silently re-rolls every saved world. A shared package that did that would be worse than the duplication it replaced.

The generator core is identical in both, so a given seed produces the same float sequence whichever interface you use — asserted in the tests.

int is inclusive of both bounds, matching sims3-clone and europe-county-map. konbini-67's local helper took a single exclusive max; it maps to nextInt(rng, 0, max - 1).

The golden-sequence test

rng.test.ts pins the first five floats for seed 12345, captured from the pre-extraction konbini-67 implementation. If that test fails, the shared core has stopped reproducing the sequence every consumer's saved seeds depend on. Regenerating those constants to make a failure go away defeats the entire point of the test — fix the code instead.

hotline3d is deliberately not a consumer

hotline3d/hotline-kernel/src/core/rng.ts looks like the same generator but uses % and Math.trunc where mulberry32 uses >>>/>>. Measured, it produces a different sequence — so adopting this package would change its generated output. Left alone on purpose.

Commands

pnpm install
pnpm test        # vitest
pnpm coverage    # 100% branches/functions/lines/statements, enforced
pnpm lint        # tsc --noEmit + eslint (type-aware, strict)
pnpm build       # tsc -> dist/ (ESM + .d.ts), committed to the tag

dist/ is committed, like web_ui's: Vite does not transpile dependencies, so a consumer would otherwise need per-repo bundler config. CI fails if the committed dist/ does not match a fresh build.