@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,98 @@
1
+ /**
2
+ * In-Memory Storage Implementation
3
+ *
4
+ * Pure JavaScript in-memory storage using Maps/Arrays.
5
+ * Ideal for testing, CI, and ephemeral workloads.
6
+ * No persistence - data lost on process exit.
7
+ */
8
+
9
+ import { Storage } from './index.js';
10
+
11
+ /**
12
+ * MemoryStorage - In-memory implementation
13
+ */
14
+ export class MemoryStorage extends Storage {
15
+ constructor() {
16
+ super();
17
+ this.settings = new Map();
18
+ this.battleHistory = [];
19
+ this.qTable = new Map(); // key: "state:action" -> q_value
20
+ this.initialized = false;
21
+ }
22
+
23
+ async initialize() {
24
+ if (this.initialized) return;
25
+ this.settings.set('is_active', '1');
26
+ this.initialized = true;
27
+ }
28
+
29
+ async close() {
30
+ // No-op for memory storage
31
+ }
32
+
33
+ async getSetting(key) {
34
+ const value = this.settings.get(key);
35
+ return value ? { key, value } : null;
36
+ }
37
+
38
+ async setSetting(key, value) {
39
+ this.settings.set(key, String(value));
40
+ }
41
+
42
+ async getQTable() {
43
+ const results = [];
44
+ for (const [key, qValue] of this.qTable) {
45
+ const [state, action] = key.split(':');
46
+ results.push({ state, action: parseInt(action, 10), q_value: qValue });
47
+ }
48
+ return results.sort((a, b) => a.state.localeCompare(b.state) || a.action - b.action);
49
+ }
50
+
51
+ async updateQ(state, action, qValue) {
52
+ this.qTable.set(`${state}:${action}`, qValue);
53
+ }
54
+
55
+ async addBattle(record) {
56
+ this.battleHistory.push({
57
+ id: this.battleHistory.length + 1,
58
+ ...record
59
+ });
60
+ }
61
+
62
+ async getTodayBattleCount() {
63
+ const today = new Date().toISOString().split('T')[0];
64
+ return this.battleHistory.filter(
65
+ r => r.createdAt.startsWith(today)
66
+ ).length;
67
+ }
68
+
69
+ async getPerformanceStats() {
70
+ const stats = new Map();
71
+
72
+ for (const record of this.battleHistory) {
73
+ const mode = record.mode;
74
+ if (!stats.has(mode)) {
75
+ stats.set(mode, { total: 0, wins: 0 });
76
+ }
77
+ const stat = stats.get(mode);
78
+ stat.total++;
79
+ if (record.reward > 0) stat.wins++;
80
+ }
81
+
82
+ return Array.from(stats.entries()).map(([mode, stat]) => ({
83
+ mode,
84
+ total: stat.total,
85
+ win_rate: stat.total > 0 ? Math.round((stat.wins / stat.total) * 1000) / 10 : 0
86
+ }));
87
+ }
88
+
89
+ /**
90
+ * Clear all data (useful for testing)
91
+ */
92
+ clear() {
93
+ this.settings.clear();
94
+ this.battleHistory = [];
95
+ this.qTable.clear();
96
+ this.settings.set('is_active', '1');
97
+ }
98
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * SQLite Storage Implementation using node:sqlite (Node.js 22+ built-in)
3
+ *
4
+ * Zero-dependency SQLite implementation using Node.js built-in module.
5
+ * Recommended for most use cases.
6
+ */
7
+
8
+ import { DatabaseSync } from 'node:sqlite';
9
+ import { Storage } from './index.js';
10
+
11
+ /**
12
+ * SqliteStorage - Uses node:sqlite (synchronous API)
13
+ */
14
+ export class SqliteStorage extends Storage {
15
+ /**
16
+ * @param {string} path - Database file path
17
+ */
18
+ constructor(path = './lsji.db') {
19
+ super();
20
+ this.path = path;
21
+ this.db = null;
22
+ }
23
+
24
+ async initialize() {
25
+ this.db = new DatabaseSync(this.path);
26
+
27
+ // Create tables
28
+ this.db.exec(`
29
+ CREATE TABLE IF NOT EXISTS settings (
30
+ key TEXT PRIMARY KEY,
31
+ value TEXT NOT NULL
32
+ );
33
+
34
+ CREATE TABLE IF NOT EXISTS battle_history (
35
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
36
+ mode TEXT NOT NULL CHECK (mode IN ('train', 'test')),
37
+ hand_a INTEGER NOT NULL,
38
+ hand_b INTEGER NOT NULL,
39
+ reward INTEGER NOT NULL,
40
+ created_at TEXT NOT NULL
41
+ );
42
+
43
+ CREATE TABLE IF NOT EXISTS q_table (
44
+ state TEXT NOT NULL,
45
+ action INTEGER NOT NULL,
46
+ q_value REAL NOT NULL DEFAULT 0,
47
+ PRIMARY KEY (state, action)
48
+ );
49
+
50
+ CREATE INDEX IF NOT EXISTS idx_battle_history_date
51
+ ON battle_history(created_at);
52
+ CREATE INDEX IF NOT EXISTS idx_battle_history_mode
53
+ ON battle_history(mode);
54
+ `);
55
+
56
+ // Initialize default settings
57
+ const stmt = this.db.prepare('INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)');
58
+ stmt.run('is_active', '1');
59
+ }
60
+
61
+ async close() {
62
+ if (this.db) {
63
+ this.db.close();
64
+ this.db = null;
65
+ }
66
+ }
67
+
68
+ async getSetting(key) {
69
+ const stmt = this.db.prepare('SELECT key, value FROM settings WHERE key = ?');
70
+ const row = stmt.get(key);
71
+ return row ? { key: row.key, value: row.value } : null;
72
+ }
73
+
74
+ async setSetting(key, value) {
75
+ const stmt = this.db.prepare(
76
+ 'INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?'
77
+ );
78
+ stmt.run(key, String(value), String(value));
79
+ }
80
+
81
+ async getQTable() {
82
+ const stmt = this.db.prepare('SELECT state, action, q_value FROM q_table ORDER BY state, action');
83
+ return stmt.all();
84
+ }
85
+
86
+ async updateQ(state, action, qValue) {
87
+ const stmt = this.db.prepare(`
88
+ INSERT INTO q_table (state, action, q_value) VALUES (?, ?, ?)
89
+ ON CONFLICT(state, action) DO UPDATE SET q_value = ?
90
+ `);
91
+ stmt.run(state, action, qValue, qValue);
92
+ }
93
+
94
+ async addBattle(record) {
95
+ const stmt = this.db.prepare(`
96
+ INSERT INTO battle_history (mode, hand_a, hand_b, reward, created_at)
97
+ VALUES (?, ?, ?, ?, ?)
98
+ `);
99
+ stmt.run(record.mode, record.handA, record.handB, record.reward, record.createdAt);
100
+ }
101
+
102
+ async getTodayBattleCount() {
103
+ const today = new Date().toISOString().split('T')[0];
104
+ const stmt = this.db.prepare(`
105
+ SELECT COUNT(*) as count FROM battle_history
106
+ WHERE date(created_at) = date(?)
107
+ `);
108
+ const row = stmt.get(today);
109
+ return row ? row.count : 0;
110
+ }
111
+
112
+ async getPerformanceStats() {
113
+ const stmt = this.db.prepare(`
114
+ SELECT
115
+ mode,
116
+ COUNT(*) as total,
117
+ ROUND(AVG(CASE WHEN reward > 0 THEN 1.0 ELSE 0.0 END) * 100, 1) as win_rate
118
+ FROM battle_history
119
+ GROUP BY mode
120
+ `);
121
+ return stmt.all();
122
+ }
123
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * QLearning Tests
3
+ *
4
+ * Verifies the TD learning logic matches original worker.js behavior.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach } from 'vitest';
8
+ import { QLearning } from '../../src/core/qlearning.js';
9
+ import { MemoryStorage } from '../../src/storage/memory.js';
10
+
11
+ describe('QLearning', () => {
12
+ let storage;
13
+ let qlearning;
14
+
15
+ beforeEach(async () => {
16
+ storage = new MemoryStorage();
17
+ await storage.initialize();
18
+ qlearning = new QLearning({ alpha: 0.1, gamma: 0.9, epsilon: 0.1, storage });
19
+ });
20
+
21
+ it('should initialize with default config', () => {
22
+ expect(qlearning.alpha).toBe(0.1);
23
+ expect(qlearning.gamma).toBe(0.9);
24
+ expect(qlearning.epsilon).toBe(0.1);
25
+ });
26
+
27
+ it('should clamp alpha, gamma, epsilon to [0, 1]', () => {
28
+ const ql = new QLearning({ alpha: 1.5, gamma: -0.5, epsilon: 2, storage });
29
+ expect(ql.alpha).toBe(1);
30
+ expect(ql.gamma).toBe(0);
31
+ expect(ql.epsilon).toBe(1);
32
+ });
33
+
34
+ it('should return 0 for unseen state-action pairs', async () => {
35
+ await qlearning.initialize();
36
+ const value = qlearning.getQValue('state1', 0);
37
+ expect(value).toBe(0);
38
+ });
39
+
40
+ it('should store and retrieve Q-values', async () => {
41
+ await qlearning.initialize();
42
+ await qlearning.setQValue('state1', 0, 0.5);
43
+ expect(qlearning.getQValue('state1', 0)).toBe(0.5);
44
+ });
45
+
46
+ it('should perform simple TD update (worker.js style)', async () => {
47
+ await qlearning.initialize();
48
+
49
+ // Original worker.js: oldQ + 0.1 * (reward - oldQ)
50
+ // Initial Q = 0, reward = 1 (win)
51
+ // New Q = 0 + 0.1 * (1 - 0) = 0.1
52
+ await qlearning.learnSimple('state1', 0, 1);
53
+ expect(qlearning.getQValue('state1', 0)).toBeCloseTo(0.1);
54
+
55
+ // Second update: oldQ = 0.1, reward = 1
56
+ // New Q = 0.1 + 0.1 * (1 - 0.1) = 0.19
57
+ await qlearning.learnSimple('state1', 0, 1);
58
+ expect(qlearning.getQValue('state1', 0)).toBeCloseTo(0.19);
59
+ });
60
+
61
+ it('should handle negative rewards (loss)', async () => {
62
+ await qlearning.initialize();
63
+
64
+ // reward = -1 (loss)
65
+ // New Q = 0 + 0.1 * (-1 - 0) = -0.1
66
+ await qlearning.learnSimple('state1', 0, -1);
67
+ expect(qlearning.getQValue('state1', 0)).toBeCloseTo(-0.1);
68
+ });
69
+
70
+ it('should handle zero rewards (draw)', async () => {
71
+ await qlearning.initialize();
72
+
73
+ // reward = 0 (draw)
74
+ // New Q = 0 + 0.1 * (0 - 0) = 0
75
+ await qlearning.learnSimple('state1', 0, 0);
76
+ expect(qlearning.getQValue('state1', 0)).toBe(0);
77
+ });
78
+
79
+ it('should select random action during exploration', async () => {
80
+ await qlearning.initialize();
81
+
82
+ // With epsilon=1, should always explore
83
+ const ql = new QLearning({ alpha: 0.1, gamma: 0.9, epsilon: 1, storage });
84
+ await ql.initialize();
85
+
86
+ const actions = new Set();
87
+ for (let i = 0; i < 100; i++) {
88
+ actions.add(await ql.act('state1', 3));
89
+ }
90
+
91
+ // Should have tried multiple actions
92
+ expect(actions.size).toBeGreaterThan(1);
93
+ });
94
+
95
+ it('should select best action during exploitation', async () => {
96
+ await qlearning.initialize();
97
+
98
+ // Set up Q-values: action 1 is best
99
+ await qlearning.setQValue('state1', 0, 0.1);
100
+ await qlearning.setQValue('state1', 1, 0.9);
101
+ await qlearning.setQValue('state1', 2, 0.3);
102
+
103
+ // With epsilon=0, should always exploit
104
+ const ql = new QLearning({ alpha: 0.1, gamma: 0.9, epsilon: 0, storage });
105
+ await ql.initialize();
106
+
107
+ const action = await ql.act('state1', 3);
108
+ expect(action).toBe(1);
109
+ });
110
+
111
+ it('should perform full TD update with next state', async () => {
112
+ await qlearning.initialize();
113
+
114
+ // Set up next state with known values
115
+ await qlearning.setQValue('nextState', 0, 0.5);
116
+ await qlearning.setQValue('nextState', 1, 0.8);
117
+
118
+ // Current: state1, action 0, Q=0
119
+ // Reward = 1, gamma = 0.9, max next Q = 0.8
120
+ // Target = 1 + 0.9 * 0.8 = 1.72
121
+ // New Q = 0 + 0.1 * (1.72 - 0) = 0.172
122
+ await qlearning.learn('state1', 0, 1, 'nextState', 2);
123
+
124
+ expect(qlearning.getQValue('state1', 0)).toBeCloseTo(0.172, 2);
125
+ });
126
+
127
+ it('should persist Q-values to storage', async () => {
128
+ await qlearning.initialize();
129
+ await qlearning.setQValue('state1', 0, 0.5);
130
+
131
+ // Create new QLearning instance with same storage
132
+ const ql2 = new QLearning({ alpha: 0.1, gamma: 0.9, epsilon: 0.1, storage });
133
+ await ql2.initialize();
134
+
135
+ expect(ql2.getQValue('state1', 0)).toBe(0.5);
136
+ });
137
+
138
+ it('should return full Q-table', async () => {
139
+ await qlearning.initialize();
140
+ await qlearning.setQValue('stateA', 0, 0.1);
141
+ await qlearning.setQValue('stateA', 1, 0.2);
142
+ await qlearning.setQValue('stateB', 0, 0.3);
143
+
144
+ const table = await qlearning.getFullQTable();
145
+ expect(table.length).toBe(3);
146
+ expect(table[0]).toEqual({ state: 'stateA', action: 0, q_value: 0.1 });
147
+ expect(table[1]).toEqual({ state: 'stateA', action: 1, q_value: 0.2 });
148
+ expect(table[2]).toEqual({ state: 'stateB', action: 0, q_value: 0.3 });
149
+ });
150
+ });
@@ -0,0 +1,81 @@
1
+ /**
2
+ * MemoryStorage Tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest';
6
+ import { MemoryStorage } from '../../src/storage/memory.js';
7
+
8
+ describe('MemoryStorage', () => {
9
+ let storage;
10
+
11
+ beforeEach(async () => {
12
+ storage = new MemoryStorage();
13
+ await storage.initialize();
14
+ });
15
+
16
+ it('should initialize with default settings', async () => {
17
+ const setting = await storage.getSetting('is_active');
18
+ expect(setting).toEqual({ key: 'is_active', value: '1' });
19
+ });
20
+
21
+ it('should get and set settings', async () => {
22
+ await storage.setSetting('test_key', 'test_value');
23
+ const setting = await storage.getSetting('test_key');
24
+ expect(setting).toEqual({ key: 'test_key', value: 'test_value' });
25
+ });
26
+
27
+ it('should return null for missing settings', async () => {
28
+ const setting = await storage.getSetting('nonexistent');
29
+ expect(setting).toBeNull();
30
+ });
31
+
32
+ it('should add and retrieve battles', async () => {
33
+ await storage.addBattle({
34
+ mode: 'test',
35
+ handA: 0,
36
+ handB: 1,
37
+ reward: 1,
38
+ createdAt: '2026-01-01T00:00:00Z'
39
+ });
40
+
41
+ const count = await storage.getTodayBattleCount();
42
+ // Note: this depends on current date, so we just check it's a number
43
+ expect(typeof count).toBe('number');
44
+ });
45
+
46
+ it('should track Q-table', async () => {
47
+ await storage.updateQ('state1', 0, 0.5);
48
+
49
+ const qTable = await storage.getQTable();
50
+ expect(qTable.length).toBe(1);
51
+ expect(qTable[0]).toEqual({ state: 'state1', action: 0, q_value: 0.5 });
52
+ });
53
+
54
+ it('should compute performance stats', async () => {
55
+ // Add some battles
56
+ await storage.addBattle({ mode: 'test', handA: 0, handB: 1, reward: 1, createdAt: new Date().toISOString() });
57
+ await storage.addBattle({ mode: 'test', handA: 0, handB: 2, reward: -1, createdAt: new Date().toISOString() });
58
+ await storage.addBattle({ mode: 'train', handA: 1, handB: 0, reward: 0, createdAt: new Date().toISOString() });
59
+
60
+ const stats = await storage.getPerformanceStats();
61
+ expect(stats.length).toBe(2); // test and train
62
+
63
+ const testStat = stats.find(s => s.mode === 'test');
64
+ expect(testStat.total).toBe(2);
65
+ expect(testStat.win_rate).toBe(50); // 1 win out of 2
66
+ });
67
+
68
+ it('should clear all data', async () => {
69
+ await storage.setSetting('test', 'value');
70
+ await storage.addBattle({ mode: 'test', handA: 0, handB: 1, reward: 1, createdAt: new Date().toISOString() });
71
+ await storage.updateQ('state1', 0, 0.5);
72
+
73
+ storage.clear();
74
+
75
+ const setting = await storage.getSetting('test');
76
+ expect(setting).toBeNull();
77
+
78
+ const count = await storage.getTodayBattleCount();
79
+ expect(count).toBe(0);
80
+ });
81
+ });