ok
This commit is contained in:
@ -10,7 +10,11 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nanostores/persistent": "^1.1.0",
|
||||
"@nanostores/react": "^1.0.0",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.544.0",
|
||||
"nanostores": "^1.0.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1"
|
||||
},
|
||||
|
||||
49
pnpm-lock.yaml
generated
49
pnpm-lock.yaml
generated
@ -8,9 +8,21 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@nanostores/persistent':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(nanostores@1.0.1)
|
||||
'@nanostores/react':
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(nanostores@1.0.1)(react@19.1.1)
|
||||
jszip:
|
||||
specifier: ^3.10.1
|
||||
version: 3.10.1
|
||||
lucide-react:
|
||||
specifier: ^0.544.0
|
||||
version: 0.544.0(react@19.1.1)
|
||||
nanostores:
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1
|
||||
react:
|
||||
specifier: ^19.1.1
|
||||
version: 19.1.1
|
||||
@ -236,6 +248,19 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@nanostores/persistent@1.1.0':
|
||||
resolution: {integrity: sha512-e6vfv7H99VkCfSoNTR/qNVMj6vXwWcsEL+LCQQamej5GK9iDefKxPCJjdOpBi1p4lNCFIQ+9VjYF1spvvc2p6A==}
|
||||
engines: {node: ^20.0.0 || >=22.0.0}
|
||||
peerDependencies:
|
||||
nanostores: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^1.0.0
|
||||
|
||||
'@nanostores/react@1.0.0':
|
||||
resolution: {integrity: sha512-eDduyNy+lbQJMg6XxZ/YssQqF6b4OXMFEZMYKPJCCmBevp1lg0g+4ZRi94qGHirMtsNfAWKNwsjOhC+q1gvC+A==}
|
||||
engines: {node: ^20.0.0 || >=22.0.0}
|
||||
peerDependencies:
|
||||
nanostores: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^1.0.0
|
||||
react: '>=18.0.0'
|
||||
|
||||
'@napi-rs/wasm-runtime@1.0.5':
|
||||
resolution: {integrity: sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==}
|
||||
|
||||
@ -1013,6 +1038,11 @@ packages:
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
lucide-react@0.544.0:
|
||||
resolution: {integrity: sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
magic-string@0.30.19:
|
||||
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
||||
|
||||
@ -1047,6 +1077,10 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanostores@1.0.1:
|
||||
resolution: {integrity: sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw==}
|
||||
engines: {node: ^20.0.0 || >=22.0.0}
|
||||
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
@ -1515,6 +1549,15 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@nanostores/persistent@1.1.0(nanostores@1.0.1)':
|
||||
dependencies:
|
||||
nanostores: 1.0.1
|
||||
|
||||
'@nanostores/react@1.0.0(nanostores@1.0.1)(react@19.1.1)':
|
||||
dependencies:
|
||||
nanostores: 1.0.1
|
||||
react: 19.1.1
|
||||
|
||||
'@napi-rs/wasm-runtime@1.0.5':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.5.0
|
||||
@ -2220,6 +2263,10 @@ snapshots:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
lucide-react@0.544.0(react@19.1.1):
|
||||
dependencies:
|
||||
react: 19.1.1
|
||||
|
||||
magic-string@0.30.19:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@ -2249,6 +2296,8 @@ snapshots:
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
nanostores@1.0.1: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
node-releases@2.0.21: {}
|
||||
|
||||
95
src/App.tsx
95
src/App.tsx
@ -1,33 +1,46 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import JSZip from 'jszip'
|
||||
import { BytebeatGenerator } from './lib/bytebeat'
|
||||
import { generateFormulaGrid } from './utils/bytebeatFormulas'
|
||||
import { BytebeatTile } from './components/BytebeatTile'
|
||||
import { EffectsBar } from './components/EffectsBar'
|
||||
import { getDefaultEffectValues } from './config/effects'
|
||||
import { EngineControls } from './components/EngineControls'
|
||||
import { getSampleRateFromIndex } from './config/effects'
|
||||
import { engineSettings, effectSettings } from './stores/settings'
|
||||
import type { EffectValues } from './types/effects'
|
||||
|
||||
function App() {
|
||||
const [formulas, setFormulas] = useState<string[][]>(() => generateFormulaGrid(100, 2))
|
||||
const engineValues = useStore(engineSettings)
|
||||
const effectValues = useStore(effectSettings)
|
||||
|
||||
const [formulas, setFormulas] = useState<string[][]>(() =>
|
||||
generateFormulaGrid(100, 2, engineValues.complexity)
|
||||
)
|
||||
const [playing, setPlaying] = useState<string | null>(null)
|
||||
const [queued, setQueued] = useState<string | null>(null)
|
||||
const [playbackPosition, setPlaybackPosition] = useState<number>(0)
|
||||
const [downloading, setDownloading] = useState(false)
|
||||
const [effectValues, setEffectValues] = useState<EffectValues>(getDefaultEffectValues())
|
||||
const generatorRef = useRef<BytebeatGenerator | null>(null)
|
||||
const animationFrameRef = useRef<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
effectSettings.setKey('masterVolume', engineValues.masterVolume)
|
||||
}, [engineValues.masterVolume])
|
||||
|
||||
const handleRandom = () => {
|
||||
if (generatorRef.current) {
|
||||
generatorRef.current.stop()
|
||||
setPlaying(null)
|
||||
}
|
||||
setFormulas(generateFormulaGrid(100, 2))
|
||||
setFormulas(generateFormulaGrid(100, 2, engineValues.complexity))
|
||||
setQueued(null)
|
||||
}
|
||||
|
||||
const playFormula = (formula: string, id: string) => {
|
||||
const sampleRate = getSampleRateFromIndex(engineValues.sampleRate)
|
||||
const duration = engineValues.loopDuration
|
||||
|
||||
if (!generatorRef.current) {
|
||||
generatorRef.current = new BytebeatGenerator({ duration: 30 })
|
||||
generatorRef.current = new BytebeatGenerator({ sampleRate, duration })
|
||||
} else {
|
||||
generatorRef.current.updateOptions({ sampleRate, duration })
|
||||
}
|
||||
|
||||
try {
|
||||
@ -76,6 +89,14 @@ function App() {
|
||||
playFormula(formula, id)
|
||||
} else {
|
||||
setQueued(id)
|
||||
if (generatorRef.current) {
|
||||
generatorRef.current.scheduleNextTrack(() => {
|
||||
const queuedFormula = formulas.flat()[parseInt(id.split('-')[0]) * 2 + parseInt(id.split('-')[1])]
|
||||
if (queuedFormula) {
|
||||
playFormula(queuedFormula, id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,14 +105,19 @@ function App() {
|
||||
}
|
||||
|
||||
|
||||
const handleEngineChange = (parameterId: string, value: number) => {
|
||||
engineSettings.setKey(parameterId, value)
|
||||
|
||||
if (parameterId === 'masterVolume' && generatorRef.current) {
|
||||
generatorRef.current.setEffects(effectValues)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEffectChange = (parameterId: string, value: number) => {
|
||||
setEffectValues(prev => {
|
||||
const newValues = { ...prev, [parameterId]: value }
|
||||
if (generatorRef.current) {
|
||||
generatorRef.current.setEffects(newValues)
|
||||
}
|
||||
return newValues
|
||||
})
|
||||
effectSettings.setKey(parameterId, value)
|
||||
if (generatorRef.current) {
|
||||
generatorRef.current.setEffects(effectValues)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownloadAll = async () => {
|
||||
@ -125,23 +151,26 @@ function App() {
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen flex flex-col bg-black overflow-hidden">
|
||||
<header className="bg-black border-b-2 border-white flex items-center justify-between px-6 py-3">
|
||||
<h1 className="font-mono text-sm tracking-[0.3em] text-white">BYTEBEAT</h1>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={handleRandom}
|
||||
className="px-6 py-2 bg-white text-black font-mono text-[11px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all"
|
||||
>
|
||||
RANDOM
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownloadAll}
|
||||
disabled={downloading}
|
||||
className="px-6 py-2 bg-black text-white border-2 border-white font-mono text-[11px] tracking-[0.2em] hover:bg-white hover:text-black transition-all disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
>
|
||||
{downloading ? 'DOWNLOADING...' : 'DOWNLOAD ALL'}
|
||||
</button>
|
||||
<header className="bg-black border-b-2 border-white px-6 py-3">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h1 className="font-mono text-sm tracking-[0.3em] text-white">BYTEBEAT</h1>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={handleRandom}
|
||||
className="px-6 py-2 bg-white text-black font-mono text-[11px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all"
|
||||
>
|
||||
RANDOM
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownloadAll}
|
||||
disabled={downloading}
|
||||
className="px-6 py-2 bg-black text-white border-2 border-white font-mono text-[11px] tracking-[0.2em] hover:bg-white hover:text-black transition-all disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
>
|
||||
{downloading ? 'DOWNLOADING...' : 'DOWNLOAD ALL'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<EngineControls values={engineValues} onChange={handleEngineChange} />
|
||||
</header>
|
||||
|
||||
<div className="flex-1 grid grid-cols-2 auto-rows-min gap-[1px] bg-white p-[1px] overflow-auto">
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { BytebeatGenerator } from '../lib/bytebeat'
|
||||
import { Download } from 'lucide-react'
|
||||
|
||||
interface BytebeatTileProps {
|
||||
formula: string
|
||||
@ -43,13 +44,13 @@ export function BytebeatTile({ formula, row, col, isPlaying, isQueued, playbackP
|
||||
</div>
|
||||
<div
|
||||
onClick={handleDownload}
|
||||
className={`px-3 py-1 text-[10px] tracking-[0.15em] border transition-all duration-150 cursor-pointer hover:scale-105 flex-shrink-0 relative z-10 ${
|
||||
className={`p-2 border transition-all duration-150 cursor-pointer hover:scale-105 flex-shrink-0 relative z-10 ${
|
||||
isPlaying
|
||||
? 'bg-black text-white border-black'
|
||||
: 'bg-white text-black border-white'
|
||||
}`}
|
||||
>
|
||||
DL
|
||||
<Download size={14} strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
47
src/components/EngineControls.tsx
Normal file
47
src/components/EngineControls.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { ENGINE_CONTROLS, SAMPLE_RATES, getComplexityLabel } from '../config/effects'
|
||||
import type { EffectValues } from '../types/effects'
|
||||
|
||||
interface EngineControlsProps {
|
||||
values: EffectValues
|
||||
onChange: (parameterId: string, value: number) => void
|
||||
}
|
||||
|
||||
export function EngineControls({ values, onChange }: EngineControlsProps) {
|
||||
const formatValue = (id: string, value: number): string => {
|
||||
switch (id) {
|
||||
case 'sampleRate':
|
||||
return `${SAMPLE_RATES[value]}Hz`
|
||||
case 'complexity':
|
||||
return getComplexityLabel(value)
|
||||
default:
|
||||
const param = ENGINE_CONTROLS[0].parameters.find(p => p.id === id)
|
||||
return `${value}${param?.unit || ''}`
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-6">
|
||||
{ENGINE_CONTROLS[0].parameters.map(param => (
|
||||
<div key={param.id} className="flex flex-col gap-1 min-w-[100px]">
|
||||
<div className="flex justify-between items-baseline">
|
||||
<label className="font-mono text-[9px] tracking-[0.15em] text-white">
|
||||
{param.label.toUpperCase()}
|
||||
</label>
|
||||
<span className="font-mono text-[9px] text-white">
|
||||
{formatValue(param.id, values[param.id] ?? param.default)}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={param.min}
|
||||
max={param.max}
|
||||
step={param.step}
|
||||
value={values[param.id] ?? param.default}
|
||||
onChange={(e) => onChange(param.id, Number(e.target.value))}
|
||||
className="w-full h-[2px] bg-white appearance-none cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,5 +1,50 @@
|
||||
import type { EffectConfig } from '../types/effects'
|
||||
|
||||
export const ENGINE_CONTROLS: EffectConfig[] = [
|
||||
{
|
||||
id: 'engine',
|
||||
name: 'Engine',
|
||||
parameters: [
|
||||
{
|
||||
id: 'sampleRate',
|
||||
label: 'Sample Rate',
|
||||
min: 0,
|
||||
max: 3,
|
||||
default: 1,
|
||||
step: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'loopDuration',
|
||||
label: 'Loop',
|
||||
min: 2,
|
||||
max: 8,
|
||||
default: 4,
|
||||
step: 2,
|
||||
unit: 's'
|
||||
},
|
||||
{
|
||||
id: 'complexity',
|
||||
label: 'Complexity',
|
||||
min: 0,
|
||||
max: 2,
|
||||
default: 1,
|
||||
step: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'masterVolume',
|
||||
label: 'Volume',
|
||||
min: 0,
|
||||
max: 100,
|
||||
default: 75,
|
||||
step: 1,
|
||||
unit: '%'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export const EFFECTS: EffectConfig[] = [
|
||||
{
|
||||
id: 'reverb',
|
||||
@ -64,4 +109,25 @@ export function getDefaultEffectValues(): Record<string, number> {
|
||||
})
|
||||
})
|
||||
return defaults
|
||||
}
|
||||
|
||||
export function getDefaultEngineValues(): Record<string, number> {
|
||||
const defaults: Record<string, number> = {}
|
||||
ENGINE_CONTROLS.forEach(control => {
|
||||
control.parameters.forEach(param => {
|
||||
defaults[param.id] = param.default
|
||||
})
|
||||
})
|
||||
return defaults
|
||||
}
|
||||
|
||||
export const SAMPLE_RATES = [4000, 8000, 16000, 22050]
|
||||
|
||||
export function getSampleRateFromIndex(index: number): number {
|
||||
return SAMPLE_RATES[index] || 8000
|
||||
}
|
||||
|
||||
export function getComplexityLabel(index: number): string {
|
||||
const labels = ['Simple', 'Medium', 'Complex']
|
||||
return labels[index] || 'Medium'
|
||||
}
|
||||
@ -22,6 +22,17 @@ export class BytebeatGenerator {
|
||||
this.duration = options.duration ?? 10
|
||||
}
|
||||
|
||||
updateOptions(options: Partial<BytebeatOptions>): void {
|
||||
if (options.sampleRate !== undefined) {
|
||||
this.sampleRate = options.sampleRate
|
||||
this.audioBuffer = null
|
||||
}
|
||||
if (options.duration !== undefined) {
|
||||
this.duration = options.duration
|
||||
this.audioBuffer = null
|
||||
}
|
||||
}
|
||||
|
||||
setFormula(formula: string): void {
|
||||
this.formula = formula
|
||||
try {
|
||||
@ -106,6 +117,25 @@ export class BytebeatGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
onLoopEnd(callback: () => void): void {
|
||||
if (this.sourceNode && !this.sourceNode.loop) {
|
||||
this.sourceNode.onended = callback
|
||||
}
|
||||
}
|
||||
|
||||
setLooping(loop: boolean): void {
|
||||
this.isLooping = loop
|
||||
}
|
||||
|
||||
scheduleNextTrack(callback: () => void): void {
|
||||
if (this.audioContext && this.sourceNode) {
|
||||
this.sourceNode.loop = false
|
||||
this.sourceNode.onended = () => {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
if (this.sourceNode && this.audioContext) {
|
||||
this.pauseTime = this.audioContext.currentTime - this.startTime
|
||||
|
||||
@ -4,6 +4,7 @@ export class EffectsChain {
|
||||
private audioContext: AudioContext
|
||||
private inputNode: GainNode
|
||||
private outputNode: GainNode
|
||||
private masterGainNode: GainNode
|
||||
|
||||
private delayNode: DelayNode
|
||||
private delayFeedbackNode: GainNode
|
||||
@ -20,6 +21,7 @@ export class EffectsChain {
|
||||
this.audioContext = audioContext
|
||||
|
||||
this.inputNode = audioContext.createGain()
|
||||
this.masterGainNode = audioContext.createGain()
|
||||
this.outputNode = audioContext.createGain()
|
||||
|
||||
this.delayNode = audioContext.createDelay(2.0)
|
||||
@ -57,7 +59,8 @@ export class EffectsChain {
|
||||
this.reverbDryNode.connect(this.tbdNode)
|
||||
this.reverbWetNode.connect(this.tbdNode)
|
||||
|
||||
this.tbdNode.connect(this.outputNode)
|
||||
this.tbdNode.connect(this.masterGainNode)
|
||||
this.masterGainNode.connect(this.outputNode)
|
||||
}
|
||||
|
||||
private generateImpulseResponse(): void {
|
||||
@ -86,6 +89,10 @@ export class EffectsChain {
|
||||
const delayAmount = Math.min(values.delayTime / 1000, 0.5)
|
||||
this.delayWetNode.gain.value = delayAmount
|
||||
this.delayDryNode.gain.value = 1 - delayAmount
|
||||
|
||||
if (values.masterVolume !== undefined) {
|
||||
this.masterGainNode.gain.value = values.masterVolume / 100
|
||||
}
|
||||
}
|
||||
|
||||
getInputNode(): AudioNode {
|
||||
@ -99,6 +106,7 @@ export class EffectsChain {
|
||||
dispose(): void {
|
||||
this.inputNode.disconnect()
|
||||
this.outputNode.disconnect()
|
||||
this.masterGainNode.disconnect()
|
||||
this.delayNode.disconnect()
|
||||
this.delayFeedbackNode.disconnect()
|
||||
this.delayWetNode.disconnect()
|
||||
|
||||
9
src/stores/settings.ts
Normal file
9
src/stores/settings.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { persistentMap } from '@nanostores/persistent'
|
||||
import { getDefaultEngineValues, getDefaultEffectValues } from '../config/effects'
|
||||
|
||||
export const engineSettings = persistentMap<Record<string, number>>('engine:', getDefaultEngineValues())
|
||||
|
||||
export const effectSettings = persistentMap<Record<string, number>>('effects:', {
|
||||
...getDefaultEffectValues(),
|
||||
masterVolume: 75
|
||||
})
|
||||
@ -1,41 +1,143 @@
|
||||
const operators = ['&', '|', '^', '+', '-', '*', '%']
|
||||
const shifts = ['>>', '<<']
|
||||
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 25, 32, 42, 63, 64, 127, 128, 255]
|
||||
interface Template {
|
||||
pattern: string
|
||||
weight: number
|
||||
}
|
||||
|
||||
const SHIFT_LOW = [4, 5, 6, 7]
|
||||
const SHIFT_MID = [8, 9, 10, 11, 12]
|
||||
const SHIFT_HIGH = [13, 14, 15, 16]
|
||||
const SHIFTS = [...SHIFT_LOW, ...SHIFT_MID, ...SHIFT_HIGH]
|
||||
|
||||
const MASKS = [1, 2, 3, 4, 5, 7, 8, 11, 13, 15, 16, 31, 32, 42, 63, 64, 127, 128, 255]
|
||||
const MULTIPLIERS = [2, 3, 5, 7, 11, 13]
|
||||
const SMALL_NUMS = [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
|
||||
const TEMPLATES: Template[] = [
|
||||
{ pattern: "t*(N&t>>S)", weight: 8 },
|
||||
{ pattern: "t*((t>>S)&N)", weight: 8 },
|
||||
{ pattern: "t*(N|(t>>S))", weight: 5 },
|
||||
{ pattern: "(t*M&t>>S1)|(t*M2&t>>S2)", weight: 10 },
|
||||
{ pattern: "t*(t>>S1|t>>S2)", weight: 10 },
|
||||
{ pattern: "t*(t>>S1&t>>S2)", weight: 8 },
|
||||
{ pattern: "(t>>S1)|(t>>S2)", weight: 7 },
|
||||
{ pattern: "(t>>S1)&(t>>S2)", weight: 7 },
|
||||
{ pattern: "t&t>>S", weight: 6 },
|
||||
{ pattern: "(t&t>>S1)*(t>>S2|t>>S3)", weight: 8 },
|
||||
{ pattern: "(t>>S)&N", weight: 5 },
|
||||
{ pattern: "t*(t>>S)", weight: 4 },
|
||||
{ pattern: "(t*M)&(t>>S)", weight: 6 },
|
||||
{ pattern: "t^(t>>S)", weight: 5 },
|
||||
{ pattern: "(t>>S1)^(t>>S2)", weight: 6 },
|
||||
{ pattern: "t*(N&(t>>S1|t>>S2))", weight: 7 },
|
||||
{ pattern: "((t>>S1)&N)*(t>>S2)", weight: 6 },
|
||||
{ pattern: "t*((t>>S1)|(t>>S2))&N", weight: 7 },
|
||||
{ pattern: "(t&(t>>S1))^(t>>S2)", weight: 5 },
|
||||
{ pattern: "t/(t%(t>>S|t>>S2))", weight: 3 },
|
||||
{ pattern: "t<<((t>>S1|t>>S2)^(t>>S3))", weight: 4 },
|
||||
{ pattern: "(t>>S)%(N)", weight: 3 },
|
||||
{ pattern: "t%(N)+(t>>S)", weight: 4 },
|
||||
{ pattern: "(t*M&t>>S1)^(t>>S2)", weight: 5 },
|
||||
{ pattern: "((t>>S1)|(t>>S2))&((t>>S3)|(t>>S4))", weight: 6 }
|
||||
]
|
||||
|
||||
const TOTAL_WEIGHT = TEMPLATES.reduce((sum, t) => sum + t.weight, 0)
|
||||
|
||||
function randomElement<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)]
|
||||
}
|
||||
|
||||
function generateTerm(depth: number = 0): string {
|
||||
if (depth > 2 || Math.random() < 0.3) {
|
||||
const shift = randomElement(shifts)
|
||||
const num = randomElement(numbers)
|
||||
return `(t${shift}${num})`
|
||||
function pickTemplate(): Template {
|
||||
let random = Math.random() * TOTAL_WEIGHT
|
||||
for (const template of TEMPLATES) {
|
||||
random -= template.weight
|
||||
if (random <= 0) {
|
||||
return template
|
||||
}
|
||||
}
|
||||
|
||||
const op = randomElement(operators)
|
||||
const left = generateTerm(depth + 1)
|
||||
const right = Math.random() < 0.5 ? generateTerm(depth + 1) : randomElement(numbers).toString()
|
||||
return `(${left}${op}${right})`
|
||||
return TEMPLATES[0]
|
||||
}
|
||||
|
||||
export function generateRandomFormula(): string {
|
||||
const numTerms = Math.floor(Math.random() * 3) + 1
|
||||
const terms: string[] = []
|
||||
function fillTemplate(pattern: string): string {
|
||||
let formula = pattern
|
||||
|
||||
for (let i = 0; i < numTerms; i++) {
|
||||
terms.push(generateTerm())
|
||||
}
|
||||
const sMatches = formula.match(/S\d*/g) || []
|
||||
const uniqueShifts = [...new Set(sMatches)]
|
||||
uniqueShifts.forEach(placeholder => {
|
||||
const shift = randomElement(SHIFTS)
|
||||
formula = formula.replace(new RegExp(placeholder, 'g'), shift.toString())
|
||||
})
|
||||
|
||||
return terms.join(randomElement(operators))
|
||||
const nMatches = formula.match(/N\d*/g) || []
|
||||
const uniqueMasks = [...new Set(nMatches)]
|
||||
uniqueMasks.forEach(placeholder => {
|
||||
const mask = randomElement(MASKS)
|
||||
formula = formula.replace(new RegExp(placeholder, 'g'), mask.toString())
|
||||
})
|
||||
|
||||
const mMatches = formula.match(/M\d*/g) || []
|
||||
const uniqueMults = [...new Set(mMatches)]
|
||||
uniqueMults.forEach(placeholder => {
|
||||
const mult = randomElement(MULTIPLIERS)
|
||||
formula = formula.replace(new RegExp(placeholder, 'g'), mult.toString())
|
||||
})
|
||||
|
||||
return formula
|
||||
}
|
||||
|
||||
export function generateFormulaGrid(rows: number, cols: number): string[][] {
|
||||
export function generateRandomFormula(complexity: number = 1): string {
|
||||
const complexityWeights = [
|
||||
{ simple: 0.6, medium: 0.3, complex: 0.1 },
|
||||
{ simple: 0.3, medium: 0.5, complex: 0.2 },
|
||||
{ simple: 0.1, medium: 0.3, complex: 0.6 }
|
||||
]
|
||||
|
||||
const weights = complexityWeights[complexity] || complexityWeights[1]
|
||||
|
||||
const filteredTemplates = TEMPLATES.map(t => {
|
||||
const patternComplexity = (t.pattern.match(/[S|N|M]\d*/g) || []).length
|
||||
let weight = t.weight
|
||||
|
||||
if (patternComplexity <= 2) {
|
||||
weight *= weights.simple
|
||||
} else if (patternComplexity <= 4) {
|
||||
weight *= weights.medium
|
||||
} else {
|
||||
weight *= weights.complex
|
||||
}
|
||||
|
||||
return { ...t, weight }
|
||||
})
|
||||
|
||||
const totalWeight = filteredTemplates.reduce((sum, t) => sum + t.weight, 0)
|
||||
let random = Math.random() * totalWeight
|
||||
|
||||
let selectedTemplate = filteredTemplates[0]
|
||||
for (const template of filteredTemplates) {
|
||||
random -= template.weight
|
||||
if (random <= 0) {
|
||||
selectedTemplate = template
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let formula = fillTemplate(selectedTemplate.pattern)
|
||||
|
||||
const extraLayerChance = complexity === 0 ? 0.05 : complexity === 1 ? 0.15 : 0.25
|
||||
if (Math.random() < extraLayerChance) {
|
||||
const op = randomElement(['&', '|', '^'])
|
||||
const num = randomElement(SMALL_NUMS)
|
||||
formula = `(${formula})${op}${num}`
|
||||
}
|
||||
|
||||
return formula
|
||||
}
|
||||
|
||||
export function generateFormulaGrid(rows: number, cols: number, complexity: number = 1): string[][] {
|
||||
const grid: string[][] = []
|
||||
for (let i = 0; i < rows; i++) {
|
||||
const row: string[] = []
|
||||
for (let j = 0; j < cols; j++) {
|
||||
row.push(generateRandomFormula())
|
||||
row.push(generateRandomFormula(complexity))
|
||||
}
|
||||
grid.push(row)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user