@game_ryo/lsji 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.github/workflows/node.yml +46 -0
  2. package/AGENTS.md +143 -0
  3. package/LICENSE +185 -0
  4. package/PROPOSAL.md +18 -0
  5. package/README.md +102 -0
  6. package/bin/lsji.js +8 -0
  7. package/docs/README.md +43 -0
  8. package/docs/blog/2019-05-28-first-blog-post.mdx +12 -0
  9. package/docs/blog/2019-05-29-long-blog-post.mdx +44 -0
  10. package/docs/blog/2021-08-01-mdx-blog-post.mdx +24 -0
  11. package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
  12. package/docs/blog/2021-08-26-welcome/index.mdx +29 -0
  13. package/docs/blog/authors.yml +25 -0
  14. package/docs/blog/tags.yml +19 -0
  15. package/docs/docs/api/agent.md +151 -0
  16. package/docs/docs/api/env.md +133 -0
  17. package/docs/docs/api/environments.md +102 -0
  18. package/docs/docs/api/qlearning.md +138 -0
  19. package/docs/docs/api/storage.md +168 -0
  20. package/docs/docs/architecture.md +155 -0
  21. package/docs/docs/cli.md +210 -0
  22. package/docs/docs/contributing.md +162 -0
  23. package/docs/docs/core-concepts.md +152 -0
  24. package/docs/docs/examples/advanced-training.md +244 -0
  25. package/docs/docs/examples/custom-environment.md +198 -0
  26. package/docs/docs/examples/custom-storage.md +251 -0
  27. package/docs/docs/getting-started.md +91 -0
  28. package/docs/docusaurus.config.ts +149 -0
  29. package/docs/package-lock.json +19522 -0
  30. package/docs/package.json +49 -0
  31. package/docs/sidebars.ts +33 -0
  32. package/docs/src/components/HomepageFeatures/index.tsx +71 -0
  33. package/docs/src/components/HomepageFeatures/styles.module.css +11 -0
  34. package/docs/src/css/custom.css +79 -0
  35. package/docs/src/pages/index.module.css +23 -0
  36. package/docs/src/pages/index.tsx +44 -0
  37. package/docs/src/pages/markdown-page.mdx +7 -0
  38. package/docs/static/.nojekyll +0 -0
  39. package/docs/static/img/docusaurus-social-card.jpg +0 -0
  40. package/docs/static/img/docusaurus.png +0 -0
  41. package/docs/static/img/favicon.ico +0 -0
  42. package/docs/static/img/logo.png +0 -0
  43. package/docs/static/img/undraw_docusaurus_mountain.svg +171 -0
  44. package/docs/static/img/undraw_docusaurus_react.svg +170 -0
  45. package/docs/static/img/undraw_docusaurus_tree.svg +40 -0
  46. package/docs/tsconfig.json +12 -0
  47. package/legacy/worker.js +166 -0
  48. package/legacy/wrangler.toml +11 -0
  49. package/package.json +26 -0
  50. package/src/cli.js +232 -0
  51. package/src/core/agent.js +239 -0
  52. package/src/core/env.js +86 -0
  53. package/src/core/qlearning.js +197 -0
  54. package/src/envs/rps.js +168 -0
  55. package/src/index.js +22 -0
  56. package/src/storage/better-sqlite.js +133 -0
  57. package/src/storage/index.js +146 -0
  58. package/src/storage/memory.js +98 -0
  59. package/src/storage/sqlite.js +123 -0
  60. package/test/core/qlearning.test.js +150 -0
  61. package/test/storage/memory.test.js +81 -0
@@ -0,0 +1,152 @@
1
+ ---
2
+ title: Core Concepts
3
+ description: Understand the core architecture of LSJI
4
+ ---
5
+
6
+ # Core Concepts
7
+
8
+ LSJI is built around four core abstractions that work together to create a flexible reinforcement learning framework.
9
+
10
+ ## Architecture Overview
11
+
12
+ ```
13
+ ┌─────────────────────────────────────────────────────────────┐
14
+ │ Agent │
15
+ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
16
+ │ │ QLearning │ │ Storage │ │ Env │ │
17
+ │ │ (Engine) │◄─┤ (Backend) │ │ (Environment) │ │
18
+ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │
19
+ └─────────────────────────────────────────────────────────────┘
20
+ ```
21
+
22
+ ### 1. Environment (`Env`)
23
+
24
+ The `Env` interface defines the problem domain. Any RL environment must implement:
25
+
26
+ ```typescript
27
+ abstract class Env {
28
+ getState(): string; // Current state representation
29
+ step(action: number): Promise<StepResult>; // Execute action
30
+ actionSize(): number; // Number of possible actions
31
+ reset(): Promise<string>; // Reset to initial state
32
+ }
33
+ ```
34
+
35
+ **StepResult** contains:
36
+ - `state` — New state after action
37
+ - `reward` — Reward received (-1, 0, 1)
38
+ - `done` — Whether episode ended
39
+ - `info` — Additional diagnostic info
40
+
41
+ ### 2. Q-Learning Engine (`QLearning`)
42
+
43
+ Tabular Q-Learning with Temporal Difference (TD) updates:
44
+
45
+ ```typescript
46
+ class QLearning {
47
+ constructor({ alpha, gamma, epsilon, storage });
48
+
49
+ // Epsilon-greedy action selection
50
+ async act(state: string, actionSize: number): Promise<number>;
51
+
52
+ // Full TD update: Q(s,a) ← Q(s,a) + α[r + γ·max Q(s',a') - Q(s,a)]
53
+ async learn(state, action, reward, nextState, nextActionSize);
54
+
55
+ // Simplified update (terminal states): Q(s,a) ← Q(s,a) + α[r - Q(s,a)]
56
+ async learnSimple(state, action, reward);
57
+
58
+ // Get all Q-values for inspection
59
+ async getFullQTable(): Promise<QTableRecord[]>;
60
+ }
61
+ ```
62
+
63
+ **Hyperparameters:**
64
+ - `alpha` (0.1) — Learning rate
65
+ - `gamma` (0.9) — Discount factor
66
+ - `epsilon` (0.1) — Exploration rate
67
+
68
+ ### 3. Storage Backend (`Storage`)
69
+
70
+ Pluggable persistence layer with three implementations:
71
+
72
+ | Backend | Package | Use Case |
73
+ |---------|---------|----------|
74
+ | `SqliteStorage` | `node:sqlite` (built-in) | **Recommended** — Zero dependencies |
75
+ | `BetterSqliteStorage` | `better-sqlite3` | High-performance synchronous access |
76
+ | `MemoryStorage` | Built-in | Testing, CI, ephemeral workloads |
77
+
78
+ All implement the same interface:
79
+ ```typescript
80
+ interface Storage {
81
+ initialize(): Promise<void>;
82
+ close(): Promise<void>;
83
+ getSetting(key): Promise<Setting>;
84
+ setSetting(key, value): Promise<void>;
85
+ getQTable(): Promise<QTableRecord[]>;
86
+ updateQ(state, action, qValue): Promise<void>;
87
+ addBattle(record): Promise<void>;
88
+ getTodayBattleCount(): Promise<number>;
89
+ getPerformanceStats(): Promise<PerformanceStat[]>;
90
+ }
91
+ ```
92
+
93
+ ### 4. Agent (`Agent`)
94
+
95
+ High-level orchestration combining all components:
96
+
97
+ ```typescript
98
+ class Agent {
99
+ constructor({ qlearning, storage, env });
100
+
101
+ async train({ episodes, actionSelector, batchSize });
102
+ async play(options?): Promise<PlayResult>;
103
+ async status(): Promise<StatusInfo>;
104
+ async start(): Promise<{status, message}>;
105
+ async stop(): Promise<{status, message}>;
106
+ setEnvironment(env): void;
107
+ }
108
+ ```
109
+
110
+ ## Data Flow
111
+
112
+ ### Training Loop
113
+ ```
114
+ for each episode:
115
+ 1. Get current state from Env
116
+ 2. Select action via QLearning.act() (ε-greedy)
117
+ 3. Execute action in Env → StepResult
118
+ 4. Update Q-table via QLearning.learnSimple()
119
+ 5. Persist battle record to Storage
120
+ 6. Batch DB writes for performance
121
+ ```
122
+
123
+ ### Play Loop
124
+ ```
125
+ 1. Get current state from Env
126
+ 2. Select best action via QLearning.act() (ε=0 for exploitation)
127
+ 3. Execute action in Env
128
+ 4. Update Q-table with result
129
+ 5. Record battle to Storage
130
+ 6. Return result
131
+ ```
132
+
133
+ ## Reward System (RPS Example)
134
+
135
+ | Outcome | Judge Formula | Reward |
136
+ |---------|---------------|--------|
137
+ | Win | (ai - user + 3) % 3 = 2 | +1 |
138
+ | Lose | (ai - user + 3) % 3 = 1 | -1 |
139
+ | Draw | (ai - user + 3) % 3 = 0 | 0 |
140
+
141
+ ## Training Patterns
142
+
143
+ Built-in patterns for the RPS environment:
144
+
145
+ | Pattern | ID | Description |
146
+ |---------|-----|-------------|
147
+ | Random | 0 | Uniform random actions |
148
+ | Always Rock | 1 | Always play action 0 |
149
+ | Counter | 2 | Play counter to previous action |
150
+ | Sequential | 3 | Cycle through 0,1,2,0,1,2... |
151
+
152
+ Custom patterns can be implemented via `actionSelector` function.
@@ -0,0 +1,244 @@
1
+ ---
2
+ title: Advanced Training
3
+ description: Custom training patterns and techniques
4
+ ---
5
+
6
+ # Advanced Training
7
+
8
+ Learn advanced training techniques for better agent performance.
9
+
10
+ ## Custom Action Selectors
11
+
12
+ The `train()` method accepts an `actionSelector` function for custom training patterns.
13
+
14
+ ```typescript
15
+ const result = await agent.train({
16
+ episodes: 1000,
17
+ actionSelector: (episode, lastAction) => {
18
+ // Your custom logic here
19
+ return action;
20
+ }
21
+ });
22
+ ```
23
+
24
+ ### Epsilon-Greedy with Decay
25
+
26
+ ```typescript
27
+ let epsilon = 1.0;
28
+ const minEpsilon = 0.01;
29
+ const decayRate = 0.9995;
30
+
31
+ const result = await agent.train({
32
+ episodes: 10000,
33
+ actionSelector: (episode, lastAction) => {
34
+ epsilon = Math.max(minEpsilon, epsilon * decayRate);
35
+
36
+ if (Math.random() < epsilon) {
37
+ return Math.floor(Math.random() * 3); // Explore
38
+ }
39
+
40
+ // Exploit: use agent's Q-learning
41
+ const state = await agent.env.getState();
42
+ return agent.qlearning.act(state, 3);
43
+ }
44
+ });
45
+ ```
46
+
47
+ ### Curriculum Learning
48
+
49
+ Start with easy opponents, progress to harder ones.
50
+
51
+ ```typescript
52
+ const opponents = ['always_rock', 'sequential', 'counter', 'random'];
53
+ const episodesPerStage = 250;
54
+
55
+ for (const opponent of opponents) {
56
+ const env = new RockPaperScissorsEnv({ opponent });
57
+ agent.setEnvironment(env);
58
+
59
+ console.log(`Training against ${opponent}...`);
60
+ await agent.train({ episodes: episodesPerStage });
61
+
62
+ const status = await agent.status();
63
+ console.log(`Win rate: ${status.performance.find(p => p.mode === 'train')?.win_rate}%`);
64
+ }
65
+ ```
66
+
67
+ ### Self-Play Training
68
+
69
+ Train agent against itself.
70
+
71
+ ```typescript
72
+ // Create two agents sharing the same Q-table
73
+ const storage = await createStorage('sqlite', { path: './selfplay.db' });
74
+ const qlearning = new QLearning({ alpha: 0.1, gamma: 0.9, epsilon: 0.1, storage });
75
+
76
+ const env1 = new RockPaperScissorsEnv({ opponent: 'random' });
77
+ const env2 = new RockPaperScissorsEnv({ opponent: 'random' });
78
+
79
+ const agent1 = new Agent({ qlearning, storage, env: env1 });
80
+ const agent2 = new Agent({ qlearning, storage, env: env2 });
81
+
82
+ // Alternate training
83
+ for (let i = 0; i < 100; i++) {
84
+ await agent1.train({ episodes: 50 });
85
+ await agent2.train({ episodes: 50 });
86
+
87
+ if (i % 10 === 0) {
88
+ const status = await agent1.status();
89
+ console.log(`Iteration ${i}: ${status.performance[0].win_rate}% win rate`);
90
+ }
91
+ }
92
+ ```
93
+
94
+ ## Hyperparameter Tuning
95
+
96
+ ### Grid Search
97
+
98
+ ```typescript
99
+ const configs = [
100
+ { alpha: 0.05, gamma: 0.9, epsilon: 0.1 },
101
+ { alpha: 0.1, gamma: 0.9, epsilon: 0.1 },
102
+ { alpha: 0.2, gamma: 0.9, epsilon: 0.1 },
103
+ { alpha: 0.1, gamma: 0.95, epsilon: 0.1 },
104
+ { alpha: 0.1, gamma: 0.9, epsilon: 0.2 },
105
+ ];
106
+
107
+ for (const config of configs) {
108
+ const storage = await createStorage('memory');
109
+ const qlearning = new QLearning({ ...config, storage });
110
+ const env = new RockPaperScissorsEnv({ opponent: 'random' });
111
+ const agent = new Agent({ qlearning, storage, env });
112
+
113
+ await agent.train({ episodes: 2000 });
114
+ const status = await agent.status();
115
+ const winRate = status.performance.find(p => p.mode === 'train')?.win_rate || 0;
116
+
117
+ console.log(`${JSON.stringify(config)} => ${winRate}%`);
118
+ await storage.close();
119
+ }
120
+ ```
121
+
122
+ ### Bayesian Optimization
123
+
124
+ Use libraries like `bayes-opt` for efficient hyperparameter search.
125
+
126
+ ## Evaluation Techniques
127
+
128
+ ### Fixed Opponent Evaluation
129
+
130
+ ```typescript
131
+ async function evaluate(agent, opponent, games = 100) {
132
+ const env = new RockPaperScissorsEnv({ opponent });
133
+ agent.setEnvironment(env);
134
+
135
+ let wins = 0, losses = 0, draws = 0;
136
+
137
+ for (let i = 0; i < games; i++) {
138
+ const result = await agent.play(Math.floor(Math.random() * 3));
139
+ if (result.reward > 0) wins++;
140
+ else if (result.reward < 0) losses++;
141
+ else draws++;
142
+ }
143
+
144
+ return { wins, losses, draws, winRate: wins / games };
145
+ }
146
+
147
+ const agents = {
148
+ random: await evaluate(agent, 'random'),
149
+ alwaysRock: await evaluate(agent, 'always_rock'),
150
+ counter: await evaluate(agent, 'counter'),
151
+ sequential: await evaluate(agent, 'sequential'),
152
+ };
153
+
154
+ console.table(agents);
155
+ ```
156
+
157
+ ### Cross-Validation
158
+
159
+ ```typescript
160
+ async function crossValidate(config, folds = 5, episodesPerFold = 1000) {
161
+ const results = [];
162
+
163
+ for (let fold = 0; fold < folds; fold++) {
164
+ const storage = await createStorage('memory');
165
+ const qlearning = new QLearning({ ...config, storage });
166
+ const env = new RockPaperScissorsEnv({ opponent: 'random' });
167
+ const agent = new Agent({ qlearning, storage, env });
168
+
169
+ await agent.train({ episodes: episodesPerFold });
170
+ const evalResult = await evaluate(agent, 'random', 200);
171
+ results.push(evalResult.winRate);
172
+
173
+ await storage.close();
174
+ }
175
+
176
+ const mean = results.reduce((a, b) => a + b, 0) / results.length;
177
+ const std = Math.sqrt(results.reduce((a, b) => a + (b - mean) ** 2, 0) / results.length);
178
+
179
+ return { mean, std, results };
180
+ }
181
+ ```
182
+
183
+ ## Checkpointing and Resuming
184
+
185
+ ```typescript
186
+ // Save Q-table periodically
187
+ async function trainWithCheckpoints(agent, episodes, checkpointEvery = 100) {
188
+ for (let i = 0; i < episodes; i += checkpointEvery) {
189
+ const batch = Math.min(checkpointEvery, episodes - i);
190
+ await agent.train({ episodes: batch });
191
+
192
+ // Q-table automatically persisted to storage
193
+ const status = await agent.status();
194
+ console.log(`Checkpoint ${i + batch}: ${status.aiBrain.length} Q-values`);
195
+ }
196
+ }
197
+
198
+ // Resume from existing Q-table
199
+ const storage = await createStorage('sqlite', { path: './existing.db' });
200
+ const qlearning = new QLearning({ alpha: 0.1, gamma: 0.9, epsilon: 0.1, storage });
201
+ // Q-table loads automatically on first use
202
+ ```
203
+
204
+ ## Distributed Training
205
+
206
+ Run multiple training processes with shared storage.
207
+
208
+ ```bash
209
+ # Terminal 1
210
+ lsji train --episodes 500 --db-path ./shared.db --storage sqlite
211
+
212
+ # Terminal 2 (same database)
213
+ lsji train --episodes 500 --db-path ./shared.db --storage sqlite
214
+
215
+ # Terminal 3
216
+ lsji train --episodes 500 --db-path ./shared.db --storage sqlite
217
+ ```
218
+
219
+ All processes read/write to the same SQLite database, enabling parallel training.
220
+
221
+ ## Monitoring Training Progress
222
+
223
+ ```typescript
224
+ async function trainWithLogging(agent, episodes) {
225
+ const history = [];
226
+
227
+ for (let i = 0; i < episodes; i += 100) {
228
+ await agent.train({ episodes: 100 });
229
+
230
+ const status = await agent.status();
231
+ const trainStat = status.performance.find(p => p.mode === 'train');
232
+
233
+ history.push({
234
+ episode: i + 100,
235
+ winRate: trainStat?.win_rate || 0,
236
+ qTableSize: status.aiBrain.length
237
+ });
238
+
239
+ console.log(`Episode ${i + 100}: ${history[history.length - 1].winRate}% win rate`);
240
+ }
241
+
242
+ return history;
243
+ }
244
+ ```
@@ -0,0 +1,198 @@
1
+ ---
2
+ title: Custom Environment
3
+ description: Build your own RL environment
4
+ ---
5
+
6
+ # Custom Environment Example
7
+
8
+ This guide shows how to create a custom environment by extending the `Env` base class.
9
+
10
+ ## Grid World Environment
11
+
12
+ A simple 1D grid where the agent learns to move right to reach the goal.
13
+
14
+ ```typescript
15
+ import { Env } from 'lsji';
16
+
17
+ class GridWorldEnv extends Env {
18
+ constructor(gridSize = 10) {
19
+ super();
20
+ this.gridSize = gridSize;
21
+ this.position = 0;
22
+ }
23
+
24
+ getState(): string {
25
+ return String(this.position);
26
+ }
27
+
28
+ async step(action: number): Promise<StepResult> {
29
+ // Actions: 0 = left, 1 = right
30
+ if (action === 0) {
31
+ this.position = Math.max(0, this.position - 1);
32
+ } else if (action === 1) {
33
+ this.position = Math.min(this.gridSize - 1, this.position + 1);
34
+ }
35
+
36
+ const done = this.position === this.gridSize - 1;
37
+ const reward = done ? 1 : -0.01; // Small penalty for each step
38
+
39
+ return {
40
+ state: String(this.position),
41
+ reward,
42
+ done,
43
+ info: { position: this.position }
44
+ };
45
+ }
46
+
47
+ actionSize(): number {
48
+ return 2; // Left, Right
49
+ }
50
+
51
+ async reset(): Promise<string> {
52
+ this.position = 0;
53
+ return '0';
54
+ }
55
+
56
+ render(): string {
57
+ const bar = ' '.repeat(this.gridSize);
58
+ const chars = bar.split('');
59
+ chars[this.position] = 'A';
60
+ chars[this.gridSize - 1] = 'G';
61
+ return `[${chars.join('')}]`;
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## Using the Custom Environment
67
+
68
+ ```typescript
69
+ import { Agent, QLearning, createStorage } from 'lsji';
70
+ import { GridWorldEnv } from './grid-world';
71
+
72
+ async function main() {
73
+ const storage = await createStorage('sqlite', { path: './gridworld.db' });
74
+
75
+ const qlearning = new QLearning({
76
+ alpha: 0.1,
77
+ gamma: 0.9,
78
+ epsilon: 0.1,
79
+ storage
80
+ });
81
+
82
+ const env = new GridWorldEnv(10);
83
+ const agent = new Agent({ qlearning, storage, env });
84
+
85
+ console.log('Training...');
86
+ await agent.train({ episodes: 5000 });
87
+
88
+ console.log('Testing...');
89
+ await agent.play(); // Single step
90
+
91
+ const status = await agent.status();
92
+ console.log('Q-table:', status.aiBrain);
93
+
94
+ await storage.close();
95
+ }
96
+
97
+ main().catch(console.error);
98
+ ```
99
+
100
+ ## Multi-State Environment
101
+
102
+ For environments with multiple state variables, use `StateEncoder`:
103
+
104
+ ```typescript
105
+ import { Env, StateEncoder } from 'lsji';
106
+
107
+ interface GameState {
108
+ playerHP: number;
109
+ enemyHP: number;
110
+ hasPotion: boolean;
111
+ }
112
+
113
+ class BattleEnv extends Env {
114
+ state: GameState = { playerHP: 100, enemyHP: 50, hasPotion: true };
115
+
116
+ getState(): string {
117
+ return StateEncoder.encode(this.state);
118
+ }
119
+
120
+ async step(action: number): Promise<StepResult> {
121
+ // 0 = attack, 1 = heal, 2 = defend
122
+ let reward = 0;
123
+ let done = false;
124
+
125
+ if (action === 0) { // Attack
126
+ this.state.enemyHP -= 10;
127
+ reward = this.state.enemyHP <= 0 ? 10 : -1;
128
+ done = this.state.enemyHP <= 0;
129
+ } else if (action === 1) { // Heal
130
+ if (this.state.hasPotion) {
131
+ this.state.playerHP = Math.min(100, this.state.playerHP + 30);
132
+ this.state.hasPotion = false;
133
+ reward = -1;
134
+ } else {
135
+ reward = -5; // No potion penalty
136
+ }
137
+ } else if (action === 2) { // Defend
138
+ reward = -0.5;
139
+ }
140
+
141
+ // Enemy counter-attack
142
+ if (!done) {
143
+ this.state.playerHP -= 5;
144
+ if (this.state.playerHP <= 0) {
145
+ reward = -10;
146
+ done = true;
147
+ }
148
+ }
149
+
150
+ return {
151
+ state: StateEncoder.encode(this.state),
152
+ reward,
153
+ done,
154
+ info: { ...this.state }
155
+ };
156
+ }
157
+
158
+ actionSize(): number {
159
+ return 3;
160
+ }
161
+
162
+ async reset(): Promise<string> {
163
+ this.state = { playerHP: 100, enemyHP: 50, hasPotion: true };
164
+ return StateEncoder.encode(this.state);
165
+ }
166
+ }
167
+ ```
168
+
169
+ ## Key Points
170
+
171
+ 1. **State as string** — Use `StateEncoder.encode()` for complex states
172
+ 2. **Reward design** — Shape rewards to guide learning (dense vs sparse)
173
+ 3. **Action space** — Keep small for tabular Q-learning
174
+ 4. **Episode termination** — Always implement `done` condition
175
+ 5. **Reset** — Must restore initial state completely
176
+
177
+ ## Testing Your Environment
178
+
179
+ ```typescript
180
+ async function testEnv() {
181
+ const env = new GridWorldEnv(5);
182
+
183
+ console.log('Initial:', env.getState());
184
+ console.log(env.render());
185
+
186
+ for (let i = 0; i < 10; i++) {
187
+ const result = await env.step(1); // Always move right
188
+ console.log(`Step ${i}: state=${result.state}, reward=${result.reward}, done=${result.done}`);
189
+ console.log(env.render());
190
+ if (result.done) break;
191
+ }
192
+
193
+ await env.reset();
194
+ console.log('After reset:', env.getState());
195
+ }
196
+
197
+ testEnv();
198
+ ```