@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.
- package/.github/workflows/node.yml +46 -0
- package/AGENTS.md +143 -0
- package/LICENSE +185 -0
- package/PROPOSAL.md +18 -0
- package/README.md +102 -0
- package/bin/lsji.js +8 -0
- package/docs/README.md +43 -0
- package/docs/blog/2019-05-28-first-blog-post.mdx +12 -0
- package/docs/blog/2019-05-29-long-blog-post.mdx +44 -0
- package/docs/blog/2021-08-01-mdx-blog-post.mdx +24 -0
- package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
- package/docs/blog/2021-08-26-welcome/index.mdx +29 -0
- package/docs/blog/authors.yml +25 -0
- package/docs/blog/tags.yml +19 -0
- package/docs/docs/api/agent.md +151 -0
- package/docs/docs/api/env.md +133 -0
- package/docs/docs/api/environments.md +102 -0
- package/docs/docs/api/qlearning.md +138 -0
- package/docs/docs/api/storage.md +168 -0
- package/docs/docs/architecture.md +155 -0
- package/docs/docs/cli.md +210 -0
- package/docs/docs/contributing.md +162 -0
- package/docs/docs/core-concepts.md +152 -0
- package/docs/docs/examples/advanced-training.md +244 -0
- package/docs/docs/examples/custom-environment.md +198 -0
- package/docs/docs/examples/custom-storage.md +251 -0
- package/docs/docs/getting-started.md +91 -0
- package/docs/docusaurus.config.ts +149 -0
- package/docs/package-lock.json +19522 -0
- package/docs/package.json +49 -0
- package/docs/sidebars.ts +33 -0
- package/docs/src/components/HomepageFeatures/index.tsx +71 -0
- package/docs/src/components/HomepageFeatures/styles.module.css +11 -0
- package/docs/src/css/custom.css +79 -0
- package/docs/src/pages/index.module.css +23 -0
- package/docs/src/pages/index.tsx +44 -0
- package/docs/src/pages/markdown-page.mdx +7 -0
- package/docs/static/.nojekyll +0 -0
- package/docs/static/img/docusaurus-social-card.jpg +0 -0
- package/docs/static/img/docusaurus.png +0 -0
- package/docs/static/img/favicon.ico +0 -0
- package/docs/static/img/logo.png +0 -0
- package/docs/static/img/undraw_docusaurus_mountain.svg +171 -0
- package/docs/static/img/undraw_docusaurus_react.svg +170 -0
- package/docs/static/img/undraw_docusaurus_tree.svg +40 -0
- package/docs/tsconfig.json +12 -0
- package/legacy/worker.js +166 -0
- package/legacy/wrangler.toml +11 -0
- package/package.json +26 -0
- package/src/cli.js +232 -0
- package/src/core/agent.js +239 -0
- package/src/core/env.js +86 -0
- package/src/core/qlearning.js +197 -0
- package/src/envs/rps.js +168 -0
- package/src/index.js +22 -0
- package/src/storage/better-sqlite.js +133 -0
- package/src/storage/index.js +146 -0
- package/src/storage/memory.js +98 -0
- package/src/storage/sqlite.js +123 -0
- package/test/core/qlearning.test.js +150 -0
- 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
|
+
```
|