@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
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
+ }
@@ -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
+ };