@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
package/src/cli.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSJI CLI - Command Line Interface
|
|
3
|
+
*
|
|
4
|
+
* Provides train/play/status/start/stop commands.
|
|
5
|
+
* Migrated from the original Cloudflare Workers HTTP endpoints.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Agent } from './core/agent.js';
|
|
9
|
+
import { QLearning } from './core/qlearning.js';
|
|
10
|
+
import { createStorage } from './storage/index.js';
|
|
11
|
+
import { RockPaperScissorsEnv, TrainingPattern, getTrainingAction } from './envs/rps.js';
|
|
12
|
+
|
|
13
|
+
// Hand names for display
|
|
14
|
+
const HAND_NAMES = ['Rock', 'Scissors', 'Paper'];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse command line arguments
|
|
18
|
+
* @returns {Object} Parsed arguments
|
|
19
|
+
*/
|
|
20
|
+
function parseArgs() {
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
const command = args[0];
|
|
23
|
+
const options = {};
|
|
24
|
+
|
|
25
|
+
for (let i = 1; i < args.length; i++) {
|
|
26
|
+
const arg = args[i];
|
|
27
|
+
if (arg.startsWith('--')) {
|
|
28
|
+
const key = arg.slice(2);
|
|
29
|
+
const nextArg = args[i + 1];
|
|
30
|
+
if (nextArg && !nextArg.startsWith('--')) {
|
|
31
|
+
options[key] = nextArg;
|
|
32
|
+
i++;
|
|
33
|
+
} else {
|
|
34
|
+
options[key] = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { command, options };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Format output as JSON or table
|
|
44
|
+
* @param {Object} data - Data to output
|
|
45
|
+
* @param {boolean} json - Whether to output JSON
|
|
46
|
+
*/
|
|
47
|
+
function output(data, json = false) {
|
|
48
|
+
if (json) {
|
|
49
|
+
console.log(JSON.stringify(data, null, 2));
|
|
50
|
+
} else if (Array.isArray(data)) {
|
|
51
|
+
console.table(data);
|
|
52
|
+
} else {
|
|
53
|
+
console.log(data);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Main CLI entry point
|
|
59
|
+
*/
|
|
60
|
+
export async function main() {
|
|
61
|
+
const { command, options } = parseArgs();
|
|
62
|
+
const jsonOutput = options.json === true || options.json === '';
|
|
63
|
+
|
|
64
|
+
// Determine storage type
|
|
65
|
+
const storageType = options.storage || process.env.LSJI_STORAGE || 'sqlite';
|
|
66
|
+
const dbPath = options.dbPath || process.env.LSJI_DB_PATH || './lsji.db';
|
|
67
|
+
|
|
68
|
+
let storage;
|
|
69
|
+
try {
|
|
70
|
+
storage = await createStorage(storageType, { path: dbPath });
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.error(`Failed to initialize storage (${storageType}):`, e.message);
|
|
73
|
+
if (storageType !== 'memory') {
|
|
74
|
+
console.error('Falling back to memory storage...');
|
|
75
|
+
storage = await createStorage('memory');
|
|
76
|
+
} else {
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Create Q-learning engine
|
|
82
|
+
const qlearning = new QLearning({
|
|
83
|
+
alpha: parseFloat(options.alpha) || 0.1,
|
|
84
|
+
gamma: parseFloat(options.gamma) || 0.9,
|
|
85
|
+
epsilon: parseFloat(options.epsilon) || 0.1,
|
|
86
|
+
storage
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Create environment (Rock-Paper-Scissors)
|
|
90
|
+
const opponent = options.opponent || 'random';
|
|
91
|
+
const env = new RockPaperScissorsEnv({ opponent });
|
|
92
|
+
|
|
93
|
+
// Create agent
|
|
94
|
+
const agent = new Agent({ qlearning, storage, env });
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
switch (command) {
|
|
98
|
+
case 'train': {
|
|
99
|
+
const episodes = parseInt(options.episodes) || 200;
|
|
100
|
+
const pattern = parseInt(options.pattern) || 0;
|
|
101
|
+
const batchSize = parseInt(options.batchSize) || 200;
|
|
102
|
+
|
|
103
|
+
// Create action selector based on pattern
|
|
104
|
+
let actionSelector = null;
|
|
105
|
+
if (pattern > 0) {
|
|
106
|
+
actionSelector = (episode, lastAction) => getTrainingAction(pattern, episode, lastAction);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log(`Training: ${episodes} episodes, pattern=${pattern}, opponent=${opponent}...`);
|
|
110
|
+
const result = await agent.train({ episodes, actionSelector, batchSize });
|
|
111
|
+
console.log('Training complete:', result);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case 'play': {
|
|
116
|
+
const hand = parseInt(options.hand);
|
|
117
|
+
if (isNaN(hand) || hand < 0 || hand > 2) {
|
|
118
|
+
console.error('Error: --hand must be 0 (Rock), 1 (Scissors), or 2 (Paper)');
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// For play, we need to pass the user's hand to the environment
|
|
123
|
+
// The RPS env uses its own opponent strategy, so we'll use a custom approach
|
|
124
|
+
const state = await env.getState() || '0';
|
|
125
|
+
const actionSize = env.actionSize();
|
|
126
|
+
const aiHand = await qlearning.act(state, actionSize);
|
|
127
|
+
|
|
128
|
+
// Calculate outcome manually for display
|
|
129
|
+
const { judge, reward, outcome } = RockPaperScissorsEnv.calculateOutcome(aiHand, hand);
|
|
130
|
+
|
|
131
|
+
// Step the environment with AI's action (to update state and Q-table)
|
|
132
|
+
const result = await env.step(aiHand);
|
|
133
|
+
|
|
134
|
+
// Update Q-table with actual result
|
|
135
|
+
await qlearning.learnSimple(state, aiHand, result.reward);
|
|
136
|
+
|
|
137
|
+
// Record battle
|
|
138
|
+
await storage.addBattle({
|
|
139
|
+
mode: 'test',
|
|
140
|
+
handA: aiHand,
|
|
141
|
+
handB: hand,
|
|
142
|
+
reward: result.reward,
|
|
143
|
+
createdAt: new Date().toISOString()
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const playResult = {
|
|
147
|
+
aiHand,
|
|
148
|
+
userHand: hand,
|
|
149
|
+
outcome,
|
|
150
|
+
aiHandName: RockPaperScissorsEnv.getHandName(aiHand),
|
|
151
|
+
userHandName: RockPaperScissorsEnv.getHandName(hand)
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
if (jsonOutput) {
|
|
155
|
+
output(playResult, true);
|
|
156
|
+
} else {
|
|
157
|
+
console.log(`You: ${playResult.userHandName} | AI: ${playResult.aiHandName} => ${playResult.outcome}`);
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case 'status': {
|
|
163
|
+
const result = await agent.status();
|
|
164
|
+
output(result, jsonOutput);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
case 'start': {
|
|
169
|
+
const result = await agent.start();
|
|
170
|
+
output(result, jsonOutput);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case 'stop': {
|
|
175
|
+
const result = await agent.stop();
|
|
176
|
+
output(result, jsonOutput);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case 'help':
|
|
181
|
+
case '--help':
|
|
182
|
+
case '-h':
|
|
183
|
+
default: {
|
|
184
|
+
console.log(`
|
|
185
|
+
LSJI CLI - Reinforcement Learning Agent Framework
|
|
186
|
+
|
|
187
|
+
Usage: lsji <command> [options]
|
|
188
|
+
|
|
189
|
+
Commands:
|
|
190
|
+
train Train the agent
|
|
191
|
+
play Play against the agent
|
|
192
|
+
status Show system status and statistics
|
|
193
|
+
start Enable training/play
|
|
194
|
+
stop Disable training/play
|
|
195
|
+
help Show this help
|
|
196
|
+
|
|
197
|
+
Options:
|
|
198
|
+
--storage <type> Storage backend: sqlite, better-sqlite, memory (default: sqlite)
|
|
199
|
+
--db-path <path> Database file path (default: ./lsji.db)
|
|
200
|
+
--alpha <number> Learning rate (default: 0.1)
|
|
201
|
+
--gamma <number> Discount factor (default: 0.9)
|
|
202
|
+
--epsilon <number> Exploration rate (default: 0.1)
|
|
203
|
+
--opponent <type> Opponent strategy: random, always_rock, counter, sequential (default: random)
|
|
204
|
+
--json Output as JSON
|
|
205
|
+
|
|
206
|
+
Train options:
|
|
207
|
+
--episodes <number> Number of episodes (default: 200)
|
|
208
|
+
--pattern <number> Training pattern 0-3 (default: 0)
|
|
209
|
+
0=random, 1=always_rock, 2=counter, 3=sequential
|
|
210
|
+
--batch-size <number> Batch size for DB (default: 200)
|
|
211
|
+
|
|
212
|
+
Play options:
|
|
213
|
+
--hand <number> Your hand: 0=Rock, 1=Scissors, 2=Paper
|
|
214
|
+
|
|
215
|
+
Examples:
|
|
216
|
+
lsji train --episodes 500
|
|
217
|
+
lsji train --episodes 100 --pattern 1 --opponent always_rock
|
|
218
|
+
lsji play --hand 0
|
|
219
|
+
lsji status --json
|
|
220
|
+
lsji start
|
|
221
|
+
lsji stop
|
|
222
|
+
`);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('Error:', error.message);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
} finally {
|
|
230
|
+
await storage.close();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-Level Agent Orchestration
|
|
3
|
+
*
|
|
4
|
+
* Combines Q-Learning engine with environment and storage.
|
|
5
|
+
* Provides train/play/status/start/stop operations.
|
|
6
|
+
* Migrated from the original Cloudflare Workers routes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { QLearning } from './qlearning.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Agent configuration
|
|
13
|
+
* @typedef {Object} AgentConfig
|
|
14
|
+
* @property {QLearning} qlearning - Q-Learning engine
|
|
15
|
+
* @property {Storage} storage - Storage backend
|
|
16
|
+
* @property {Env} [env] - Environment (optional, for play/train modes)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Training result
|
|
21
|
+
* @typedef {Object} TrainResult
|
|
22
|
+
* @property {number} episodes - Number of episodes completed
|
|
23
|
+
* @property {number} wins - Number of wins (reward > 0)
|
|
24
|
+
* @property {number} losses - Number of losses (reward < 0)
|
|
25
|
+
* @property {number} draws - Number of draws (reward = 0)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Play result
|
|
30
|
+
* @typedef {Object} PlayResult
|
|
31
|
+
* @property {number} action - Agent's chosen action
|
|
32
|
+
* @property {number} reward - Reward received
|
|
33
|
+
* @property {boolean} done - Whether episode ended
|
|
34
|
+
* @property {Object} info - Additional info from environment
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Status information
|
|
39
|
+
* @typedef {Object} StatusInfo
|
|
40
|
+
* @property {'running'|'stopped'} status - System status
|
|
41
|
+
* @property {number} todayTotal - Battles today
|
|
42
|
+
* @property {number} limit - Daily limit
|
|
43
|
+
* @property {Array} performance - Performance by mode
|
|
44
|
+
* @property {Array} aiBrain - Full Q-table
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Agent - Main orchestration class for RL agent
|
|
49
|
+
*/
|
|
50
|
+
export class Agent {
|
|
51
|
+
/**
|
|
52
|
+
* @param {AgentConfig} config
|
|
53
|
+
*/
|
|
54
|
+
constructor({ qlearning, storage, env = null } = {}) {
|
|
55
|
+
if (!qlearning || !storage) {
|
|
56
|
+
throw new Error('QLearning and Storage are required');
|
|
57
|
+
}
|
|
58
|
+
this.qlearning = qlearning;
|
|
59
|
+
this.storage = storage;
|
|
60
|
+
this.env = env;
|
|
61
|
+
this.isActive = true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if system is active
|
|
66
|
+
* @returns {Promise<boolean>}
|
|
67
|
+
*/
|
|
68
|
+
async checkActive() {
|
|
69
|
+
const config = await this.storage.getSetting('is_active');
|
|
70
|
+
this.isActive = config ? config.value === '1' : true;
|
|
71
|
+
return this.isActive;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Start the system (enable training/play)
|
|
76
|
+
* @returns {Promise<{status: string, message: string}>}
|
|
77
|
+
*/
|
|
78
|
+
async start() {
|
|
79
|
+
await this.storage.setSetting('is_active', 1);
|
|
80
|
+
this.isActive = true;
|
|
81
|
+
return { status: 'success', message: 'System STARTED' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Stop the system (disable training/play)
|
|
86
|
+
* @returns {Promise<{status: string, message: string}>}
|
|
87
|
+
*/
|
|
88
|
+
async stop() {
|
|
89
|
+
await this.storage.setSetting('is_active', 0);
|
|
90
|
+
this.isActive = false;
|
|
91
|
+
return { status: 'success', message: 'System STOPPED' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get current status and statistics
|
|
96
|
+
* @returns {Promise<StatusInfo>}
|
|
97
|
+
*/
|
|
98
|
+
async status() {
|
|
99
|
+
await this.checkActive();
|
|
100
|
+
|
|
101
|
+
const todayCount = await this.storage.getTodayBattleCount();
|
|
102
|
+
const stats = await this.storage.getPerformanceStats();
|
|
103
|
+
const brain = await this.qlearning.getFullQTable();
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
status: this.isActive ? 'running' : 'stopped',
|
|
107
|
+
todayTotal: todayCount,
|
|
108
|
+
limit: 90000,
|
|
109
|
+
performance: stats,
|
|
110
|
+
aiBrain: brain
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Train the agent with specified episodes
|
|
116
|
+
*
|
|
117
|
+
* @param {Object} options
|
|
118
|
+
* @param {number} [options.episodes=200] - Number of training episodes
|
|
119
|
+
* @param {Function} [options.actionSelector] - Function(episode, lastAction) -> action
|
|
120
|
+
* @param {number} [options.batchSize=200] - Batch size for DB operations
|
|
121
|
+
* @returns {Promise<TrainResult>}
|
|
122
|
+
*/
|
|
123
|
+
async train({ episodes = 200, actionSelector = null, batchSize = 200 } = {}) {
|
|
124
|
+
await this.checkActive();
|
|
125
|
+
if (!this.isActive) {
|
|
126
|
+
throw new Error('System is paused. Use start() first.');
|
|
127
|
+
}
|
|
128
|
+
if (!this.env) {
|
|
129
|
+
throw new Error('Environment is required for training');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let wins = 0, losses = 0, draws = 0;
|
|
133
|
+
const batchOperations = [];
|
|
134
|
+
let lastAction = 0;
|
|
135
|
+
|
|
136
|
+
for (let i = 0; i < episodes; i++) {
|
|
137
|
+
// Determine action - use custom selector or default to random
|
|
138
|
+
let action;
|
|
139
|
+
if (actionSelector) {
|
|
140
|
+
action = actionSelector(i, lastAction);
|
|
141
|
+
} else {
|
|
142
|
+
action = Math.floor(Math.random() * this.env.actionSize());
|
|
143
|
+
}
|
|
144
|
+
lastAction = action;
|
|
145
|
+
|
|
146
|
+
// Get current state
|
|
147
|
+
const state = await this.env.getState() || '0';
|
|
148
|
+
|
|
149
|
+
// Execute action in environment
|
|
150
|
+
const result = await this.env.step(action);
|
|
151
|
+
|
|
152
|
+
// Track reward
|
|
153
|
+
const reward = result.reward;
|
|
154
|
+
if (reward > 0) wins++;
|
|
155
|
+
else if (reward < 0) losses++;
|
|
156
|
+
else draws++;
|
|
157
|
+
|
|
158
|
+
// Q-learning update
|
|
159
|
+
await this.qlearning.learnSimple(state, action, reward);
|
|
160
|
+
|
|
161
|
+
// Batch database operations
|
|
162
|
+
batchOperations.push(this.storage.addBattle({
|
|
163
|
+
mode: 'train',
|
|
164
|
+
handA: action,
|
|
165
|
+
handB: result.info?.opponentAction ?? 0,
|
|
166
|
+
reward,
|
|
167
|
+
createdAt: new Date().toISOString()
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
// Flush batch
|
|
171
|
+
if (batchOperations.length >= batchSize) {
|
|
172
|
+
await Promise.all(batchOperations);
|
|
173
|
+
batchOperations.length = 0;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Flush remaining
|
|
178
|
+
if (batchOperations.length > 0) {
|
|
179
|
+
await Promise.all(batchOperations);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { episodes, wins, losses, draws };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Play a single step against the agent
|
|
187
|
+
* Uses epsilon-greedy policy for action selection.
|
|
188
|
+
*
|
|
189
|
+
* @param {Object} [options] - Play options
|
|
190
|
+
* @param {number} [options.userAction] - Optional user action (for envs that need it)
|
|
191
|
+
* @returns {Promise<PlayResult>}
|
|
192
|
+
*/
|
|
193
|
+
async play(options = {}) {
|
|
194
|
+
await this.checkActive();
|
|
195
|
+
if (!this.isActive) {
|
|
196
|
+
throw new Error('System is paused. Use start() first.');
|
|
197
|
+
}
|
|
198
|
+
if (!this.env) {
|
|
199
|
+
throw new Error('Environment is required for play mode');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Get current state
|
|
203
|
+
const state = await this.env.getState() || '0';
|
|
204
|
+
const actionSize = this.env.actionSize();
|
|
205
|
+
|
|
206
|
+
// Epsilon-greedy action selection
|
|
207
|
+
const action = await this.qlearning.act(state, actionSize);
|
|
208
|
+
|
|
209
|
+
// Execute in environment
|
|
210
|
+
const result = await this.env.step(action);
|
|
211
|
+
|
|
212
|
+
// Update Q-table
|
|
213
|
+
await this.qlearning.learnSimple(state, action, result.reward);
|
|
214
|
+
|
|
215
|
+
// Record battle
|
|
216
|
+
await this.storage.addBattle({
|
|
217
|
+
mode: 'test',
|
|
218
|
+
handA: action,
|
|
219
|
+
handB: result.info?.opponentAction ?? options.userAction ?? 0,
|
|
220
|
+
reward: result.reward,
|
|
221
|
+
createdAt: new Date().toISOString()
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
action,
|
|
226
|
+
reward: result.reward,
|
|
227
|
+
done: result.done,
|
|
228
|
+
info: result.info
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Set the environment (for dependency injection)
|
|
234
|
+
* @param {Env} env
|
|
235
|
+
*/
|
|
236
|
+
setEnvironment(env) {
|
|
237
|
+
this.env = env;
|
|
238
|
+
}
|
|
239
|
+
}
|
package/src/core/env.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Interface for Reinforcement Learning
|
|
3
|
+
*
|
|
4
|
+
* All RL environments must implement this interface.
|
|
5
|
+
* The agent interacts with the environment through this contract.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Result of a single environment step
|
|
10
|
+
* @typedef {Object} StepResult
|
|
11
|
+
* @property {string} state - The new state after taking the action
|
|
12
|
+
* @property {number} reward - The reward received for the action
|
|
13
|
+
* @property {boolean} done - Whether the episode has ended
|
|
14
|
+
* @property {Object} [info] - Additional diagnostic information
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Base Environment Class
|
|
19
|
+
*
|
|
20
|
+
* @abstract
|
|
21
|
+
*/
|
|
22
|
+
export class Env {
|
|
23
|
+
/**
|
|
24
|
+
* Get the current state representation
|
|
25
|
+
* @returns {string} Current state identifier
|
|
26
|
+
*/
|
|
27
|
+
getState() {
|
|
28
|
+
throw new Error('getState() must be implemented by subclass');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Execute an action in the environment
|
|
33
|
+
* @param {number} action - Action to execute
|
|
34
|
+
* @returns {Promise<StepResult>} Result of the step
|
|
35
|
+
*/
|
|
36
|
+
async step(action) {
|
|
37
|
+
throw new Error('step() must be implemented by subclass');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the number of possible actions
|
|
42
|
+
* @returns {number} Action space size
|
|
43
|
+
*/
|
|
44
|
+
actionSize() {
|
|
45
|
+
throw new Error('actionSize() must be implemented by subclass');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Reset the environment to initial state
|
|
50
|
+
* @returns {Promise<string>} Initial state
|
|
51
|
+
*/
|
|
52
|
+
async reset() {
|
|
53
|
+
throw new Error('reset() must be implemented by subclass');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Render the environment (optional, for debugging/visualization)
|
|
58
|
+
* @returns {string} Human-readable representation
|
|
59
|
+
*/
|
|
60
|
+
render() {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* State encoder/decoder utilities for tabular Q-learning
|
|
67
|
+
*/
|
|
68
|
+
export const StateEncoder = {
|
|
69
|
+
/**
|
|
70
|
+
* Encode a state object to string key
|
|
71
|
+
* @param {Object} state - State object
|
|
72
|
+
* @returns {string} Encoded state key
|
|
73
|
+
*/
|
|
74
|
+
encode(state) {
|
|
75
|
+
return JSON.stringify(state);
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Decode a state key to object
|
|
80
|
+
* @param {string} key - Encoded state key
|
|
81
|
+
* @returns {Object} Decoded state
|
|
82
|
+
*/
|
|
83
|
+
decode(key) {
|
|
84
|
+
return JSON.parse(key);
|
|
85
|
+
}
|
|
86
|
+
};
|