@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,197 @@
1
+ /**
2
+ * Q-Learning Engine (Temporal Difference Learning)
3
+ *
4
+ * Migrated from the original Cloudflare Workers implementation.
5
+ * Implements tabular Q-learning with epsilon-greedy exploration.
6
+ */
7
+
8
+ import { StateEncoder } from './env.js';
9
+
10
+ /**
11
+ * Q-Learning configuration
12
+ * @typedef {Object} QLearningConfig
13
+ * @property {number} [alpha=0.1] - Learning rate (0.0 to 1.0)
14
+ * @property {number} [gamma=0.9] - Discount factor (0.0 to 1.0)
15
+ * @property {number} [epsilon=0.1] - Exploration rate (0.0 to 1.0)
16
+ * @property {Storage} storage - Storage backend for Q-table persistence
17
+ */
18
+
19
+ /**
20
+ * Q-Learning Engine for Tabular Reinforcement Learning
21
+ */
22
+ export class QLearning {
23
+ /**
24
+ * @param {QLearningConfig} config
25
+ */
26
+ constructor({ alpha = 0.1, gamma = 0.9, epsilon = 0.1, storage } = {}) {
27
+ if (!storage) {
28
+ throw new Error('Storage is required for QLearning');
29
+ }
30
+
31
+ this.alpha = Math.max(0, Math.min(1, alpha));
32
+ this.gamma = Math.max(0, Math.min(1, gamma));
33
+ this.epsilon = Math.max(0, Math.min(1, epsilon));
34
+ this.storage = storage;
35
+
36
+ // In-memory Q-table cache for performance
37
+ this.qTable = new Map();
38
+ this.initialized = false;
39
+ }
40
+
41
+ /**
42
+ * Initialize Q-table from storage
43
+ * @returns {Promise<void>}
44
+ */
45
+ async initialize() {
46
+ if (this.initialized) return;
47
+
48
+ const records = await this.storage.getQTable();
49
+ for (const record of records) {
50
+ const key = `${record.state}:${record.action}`;
51
+ this.qTable.set(key, record.q_value);
52
+ }
53
+ this.initialized = true;
54
+ }
55
+
56
+ /**
57
+ * Get Q-value for state-action pair
58
+ * @param {string} state - State key
59
+ * @param {number} action - Action
60
+ * @returns {number} Q-value (0 if unseen)
61
+ */
62
+ getQValue(state, action) {
63
+ return this.qTable.get(`${state}:${action}`) ?? 0;
64
+ }
65
+
66
+ /**
67
+ * Set Q-value for state-action pair
68
+ * @param {string} state - State key
69
+ * @param {number} action - Action
70
+ * @param {number} value - Q-value
71
+ * @returns {Promise<void>}
72
+ */
73
+ async setQValue(state, action, value) {
74
+ const key = `${state}:${action}`;
75
+ this.qTable.set(key, value);
76
+ await this.storage.updateQ(state, action, value);
77
+ }
78
+
79
+ /**
80
+ * Select action using epsilon-greedy policy
81
+ * @param {string} state - Current state
82
+ * @param {number} actionSize - Number of possible actions
83
+ * @returns {Promise<number>} Selected action
84
+ */
85
+ async act(state, actionSize) {
86
+ await this.initialize();
87
+
88
+ // Exploration: random action
89
+ if (Math.random() < this.epsilon) {
90
+ return Math.floor(Math.random() * actionSize);
91
+ }
92
+
93
+ // Exploitation: best known action
94
+ let bestAction = 0;
95
+ let bestValue = -Infinity;
96
+
97
+ for (let action = 0; action < actionSize; action++) {
98
+ const value = this.getQValue(state, action);
99
+ if (value > bestValue || (value === bestValue && Math.random() < 0.5)) {
100
+ bestValue = value;
101
+ bestAction = action;
102
+ }
103
+ }
104
+
105
+ return bestAction;
106
+ }
107
+
108
+ /**
109
+ * Update Q-value using TD learning rule
110
+ * Q(s,a) <- Q(s,a) + alpha * (reward + gamma * max_a' Q(s',a') - Q(s,a))
111
+ *
112
+ * @param {string} state - Current state
113
+ * @param {number} action - Action taken
114
+ * @param {number} reward - Reward received
115
+ * @param {string} nextState - Next state
116
+ * @param {number} nextActionSize - Number of actions in next state
117
+ * @returns {Promise<number>} Updated Q-value
118
+ */
119
+ async learn(state, action, reward, nextState, nextActionSize) {
120
+ await this.initialize();
121
+
122
+ const currentQ = this.getQValue(state, action);
123
+
124
+ // Find max Q-value for next state
125
+ let maxNextQ = 0;
126
+ for (let a = 0; a < nextActionSize; a++) {
127
+ const q = this.getQValue(nextState, a);
128
+ if (q > maxNextQ) maxNextQ = q;
129
+ }
130
+
131
+ // TD update: Q(s,a) = Q(s,a) + alpha * (reward + gamma * max Q(s',a') - Q(s,a))
132
+ const target = reward + this.gamma * maxNextQ;
133
+ const newQ = currentQ + this.alpha * (target - currentQ);
134
+
135
+ await this.setQValue(state, action, newQ);
136
+ return newQ;
137
+ }
138
+
139
+ /**
140
+ * Simple Q-value update (original worker.js style)
141
+ * Q(s,a) <- Q(s,a) + alpha * (reward - Q(s,a))
142
+ * Used for terminal states or simplified updates
143
+ *
144
+ * @param {string} state - State
145
+ * @param {number} action - Action
146
+ * @param {number} reward - Reward
147
+ * @returns {Promise<number>} Updated Q-value
148
+ */
149
+ async learnSimple(state, action, reward) {
150
+ await this.initialize();
151
+
152
+ const currentQ = this.getQValue(state, action);
153
+ // Original formula: oldQ + 0.1 * (reward - oldQ)
154
+ const newQ = currentQ + this.alpha * (reward - currentQ);
155
+
156
+ await this.setQValue(state, action, newQ);
157
+ return newQ;
158
+ }
159
+
160
+ /**
161
+ * Reset Q-table (clear all learned values)
162
+ * @returns {Promise<void>}
163
+ */
164
+ async reset() {
165
+ this.qTable.clear();
166
+ // Note: storage reset would require a new method
167
+ this.initialized = false;
168
+ }
169
+
170
+ /**
171
+ * Get all Q-values for a state
172
+ * @param {string} state - State key
173
+ * @param {number} actionSize - Number of actions
174
+ * @returns {Object<string, number>} Action -> Q-value mapping
175
+ */
176
+ getStateValues(state, actionSize) {
177
+ const values = {};
178
+ for (let action = 0; action < actionSize; action++) {
179
+ values[action] = this.getQValue(state, action);
180
+ }
181
+ return values;
182
+ }
183
+
184
+ /**
185
+ * Get entire Q-table (for inspection/debugging)
186
+ * @returns {Promise<Array<{state: string, action: number, q_value: number}>>}
187
+ */
188
+ async getFullQTable() {
189
+ await this.initialize();
190
+ const results = [];
191
+ for (const [key, value] of this.qTable) {
192
+ const [state, action] = key.split(':');
193
+ results.push({ state, action: parseInt(action, 10), q_value: value });
194
+ }
195
+ return results.sort((a, b) => a.state.localeCompare(b.state) || a.action - b.action);
196
+ }
197
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Rock-Paper-Scissors Environment
3
+ *
4
+ * Implements the Env interface for the classic RPS game.
5
+ * Migrated from the original Cloudflare Workers implementation.
6
+ *
7
+ * State: The opponent's last action (0=Rock, 1=Scissors, 2=Paper)
8
+ * Actions: 0=Rock, 1=Scissors, 2=Paper
9
+ * Rewards: Win=1, Lose=-1, Draw=0
10
+ */
11
+
12
+ import { Env } from '../core/env.js';
13
+
14
+ const HAND_NAMES = ['Rock', 'Scissors', 'Paper'];
15
+
16
+ /**
17
+ * Opponent strategy type
18
+ * @typedef {'random'|'always_rock'|'counter'|'sequential'} OpponentStrategy
19
+ */
20
+
21
+ /**
22
+ * RockPaperScissorsEnv - Classic RPS environment
23
+ */
24
+ export class RockPaperScissorsEnv extends Env {
25
+ /**
26
+ * @param {Object} options
27
+ * @param {OpponentStrategy} [options.opponent='random'] - Opponent strategy
28
+ */
29
+ constructor({ opponent = 'random' } = {}) {
30
+ super();
31
+ this.opponent = opponent;
32
+ this.lastOpponentAction = 0;
33
+ this.episodeCount = 0;
34
+ }
35
+
36
+ /**
37
+ * Get current state (opponent's last action)
38
+ * @returns {string} State key
39
+ */
40
+ getState() {
41
+ return String(this.lastOpponentAction);
42
+ }
43
+
44
+ /**
45
+ * Execute action against opponent
46
+ * @param {number} action - Agent's action (0, 1, 2)
47
+ * @returns {Promise<StepResult>} Step result
48
+ */
49
+ async step(action) {
50
+ // Determine opponent action based on strategy
51
+ let opponentAction;
52
+ switch (this.opponent) {
53
+ case 'always_rock':
54
+ opponentAction = 0;
55
+ break;
56
+ case 'counter':
57
+ // Counter agent's previous action
58
+ opponentAction = this.episodeCount === 0 ? 0 : (action + 2) % 3;
59
+ break;
60
+ case 'sequential':
61
+ opponentAction = this.episodeCount % 3;
62
+ break;
63
+ case 'random':
64
+ default:
65
+ opponentAction = Math.floor(Math.random() * 3);
66
+ }
67
+
68
+ // Calculate reward from agent's perspective
69
+ // judge = (agent - opponent + 3) % 3
70
+ // 2 = agent wins, 1 = agent loses, 0 = draw
71
+ const judge = (action - opponentAction + 3) % 3;
72
+ const reward = judge === 2 ? 1 : judge === 1 ? -1 : 0;
73
+
74
+ // Update state for next step
75
+ this.lastOpponentAction = opponentAction;
76
+ this.episodeCount++;
77
+
78
+ return {
79
+ state: String(opponentAction),
80
+ reward,
81
+ done: false,
82
+ info: { opponentAction, judge }
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Number of possible actions (Rock, Paper, Scissors)
88
+ * @returns {number} 3
89
+ */
90
+ actionSize() {
91
+ return 3;
92
+ }
93
+
94
+ /**
95
+ * Reset environment to initial state
96
+ * @returns {Promise<string>} Initial state
97
+ */
98
+ async reset() {
99
+ this.lastOpponentAction = 0;
100
+ this.episodeCount = 0;
101
+ return '0';
102
+ }
103
+
104
+ /**
105
+ * Render current state
106
+ * @returns {string} Human-readable representation
107
+ */
108
+ render() {
109
+ return `RPS Env | Last opponent: ${HAND_NAMES[this.lastOpponentAction]} | Episodes: ${this.episodeCount}`;
110
+ }
111
+
112
+ /**
113
+ * Get human-readable hand name
114
+ * @param {number} hand - Hand index (0, 1, 2)
115
+ * @returns {string} Hand name
116
+ */
117
+ static getHandName(hand) {
118
+ return HAND_NAMES[hand] || 'Unknown';
119
+ }
120
+
121
+ /**
122
+ * Calculate outcome between two hands
123
+ * @param {number} agentHand - Agent's hand
124
+ * @param {number} opponentHand - Opponent's hand
125
+ * @returns {{judge: number, reward: number, outcome: string}} Result
126
+ */
127
+ static calculateOutcome(agentHand, opponentHand) {
128
+ const judge = (agentHand - opponentHand + 3) % 3;
129
+ const reward = judge === 2 ? 1 : judge === 1 ? -1 : 0;
130
+ const outcome = judge === 2 ? 'WIN' : judge === 1 ? 'LOSE' : 'DRAW';
131
+ return { judge, reward, outcome };
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Training pattern definitions (matching original worker.js)
137
+ * @readonly
138
+ * @enum {number}
139
+ */
140
+ export const TrainingPattern = {
141
+ RANDOM: 0, // Random actions
142
+ ALWAYS_ROCK: 1, // Always play Rock
143
+ COUNTER: 2, // Counter previous action
144
+ SEQUENTIAL: 3 // Sequential 0,1,2,0,1,2...
145
+ };
146
+
147
+ /**
148
+ * Get action for training pattern
149
+ * @param {number} pattern - Training pattern (0-3)
150
+ * @param {number} episode - Current episode number
151
+ * @param {number} lastAction - Last action taken
152
+ * @returns {number} Action to take
153
+ */
154
+ export function getTrainingAction(pattern, episode, lastAction = 0) {
155
+ switch (pattern) {
156
+ case TrainingPattern.ALWAYS_ROCK:
157
+ return 0;
158
+ case TrainingPattern.COUNTER:
159
+ return episode === 0 ? 0 : (lastAction + 2) % 3;
160
+ case TrainingPattern.SEQUENTIAL:
161
+ return episode % 3;
162
+ case TrainingPattern.RANDOM:
163
+ default:
164
+ return Math.floor(Math.random() * 3);
165
+ }
166
+ }
167
+
168
+ export default RockPaperScissorsEnv;
package/src/index.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * LSJI - Main Entry Point
3
+ *
4
+ * Public API exports for the reinforcement learning framework.
5
+ */
6
+
7
+ // Core
8
+ export { Env, StateEncoder } from './core/env.js';
9
+ export { QLearning } from './core/qlearning.js';
10
+ export { Agent } from './core/agent.js';
11
+
12
+ // Storage
13
+ export { Storage, createStorage } from './storage/index.js';
14
+ export { SqliteStorage } from './storage/sqlite.js';
15
+ export { BetterSqliteStorage } from './storage/better-sqlite.js';
16
+ export { MemoryStorage } from './storage/memory.js';
17
+
18
+ // Environments
19
+ export { RockPaperScissorsEnv, TrainingPattern, getTrainingAction } from './envs/rps.js';
20
+
21
+ // Version
22
+ export const VERSION = '0.1.0';
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Better-SQLite3 Storage Implementation
3
+ *
4
+ * High-performance synchronous SQLite using better-sqlite3 package.
5
+ * Install with: npm install better-sqlite3
6
+ */
7
+
8
+ import { Storage } from './index.js';
9
+
10
+ /**
11
+ * BetterSqliteStorage - Uses better-sqlite3
12
+ * Falls back gracefully if package not installed
13
+ */
14
+ export class BetterSqliteStorage 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
+ this.Database = null;
23
+ }
24
+
25
+ async initialize() {
26
+ try {
27
+ // Dynamic import to avoid hard dependency
28
+ const module = await import('better-sqlite3');
29
+ this.Database = module.default;
30
+ } catch (e) {
31
+ throw new Error('better-sqlite3 not installed. Run: npm install better-sqlite3');
32
+ }
33
+
34
+ this.db = new this.Database(this.path);
35
+
36
+ // Create tables (same schema as sqlite.js)
37
+ this.db.exec(`
38
+ CREATE TABLE IF NOT EXISTS settings (
39
+ key TEXT PRIMARY KEY,
40
+ value TEXT NOT NULL
41
+ );
42
+
43
+ CREATE TABLE IF NOT EXISTS battle_history (
44
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
45
+ mode TEXT NOT NULL CHECK (mode IN ('train', 'test')),
46
+ hand_a INTEGER NOT NULL,
47
+ hand_b INTEGER NOT NULL,
48
+ reward INTEGER NOT NULL,
49
+ created_at TEXT NOT NULL
50
+ );
51
+
52
+ CREATE TABLE IF NOT EXISTS q_table (
53
+ state TEXT NOT NULL,
54
+ action INTEGER NOT NULL,
55
+ q_value REAL NOT NULL DEFAULT 0,
56
+ PRIMARY KEY (state, action)
57
+ );
58
+
59
+ CREATE INDEX IF NOT EXISTS idx_battle_history_date
60
+ ON battle_history(created_at);
61
+ CREATE INDEX IF NOT EXISTS idx_battle_history_mode
62
+ ON battle_history(mode);
63
+ `);
64
+
65
+ // Initialize default settings
66
+ const stmt = this.db.prepare('INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)');
67
+ stmt.run('is_active', '1');
68
+ }
69
+
70
+ async close() {
71
+ if (this.db) {
72
+ this.db.close();
73
+ this.db = null;
74
+ }
75
+ }
76
+
77
+ async getSetting(key) {
78
+ const stmt = this.db.prepare('SELECT key, value FROM settings WHERE key = ?');
79
+ const row = stmt.get(key);
80
+ return row ? { key: row.key, value: row.value } : null;
81
+ }
82
+
83
+ async setSetting(key, value) {
84
+ const stmt = this.db.prepare(`
85
+ INSERT INTO settings (key, value) VALUES (?, ?)
86
+ ON CONFLICT(key) DO UPDATE SET value = ?
87
+ `);
88
+ stmt.run(key, String(value), String(value));
89
+ }
90
+
91
+ async getQTable() {
92
+ const stmt = this.db.prepare('SELECT state, action, q_value FROM q_table ORDER BY state, action');
93
+ return stmt.all();
94
+ }
95
+
96
+ async updateQ(state, action, qValue) {
97
+ const stmt = this.db.prepare(`
98
+ INSERT INTO q_table (state, action, q_value) VALUES (?, ?, ?)
99
+ ON CONFLICT(state, action) DO UPDATE SET q_value = ?
100
+ `);
101
+ stmt.run(state, action, qValue, qValue);
102
+ }
103
+
104
+ async addBattle(record) {
105
+ const stmt = this.db.prepare(`
106
+ INSERT INTO battle_history (mode, hand_a, hand_b, reward, created_at)
107
+ VALUES (?, ?, ?, ?, ?)
108
+ `);
109
+ stmt.run(record.mode, record.handA, record.handB, record.reward, record.createdAt);
110
+ }
111
+
112
+ async getTodayBattleCount() {
113
+ const today = new Date().toISOString().split('T')[0];
114
+ const stmt = this.db.prepare(`
115
+ SELECT COUNT(*) as count FROM battle_history
116
+ WHERE date(created_at) = date(?)
117
+ `);
118
+ const row = stmt.get(today);
119
+ return row ? row.count : 0;
120
+ }
121
+
122
+ async getPerformanceStats() {
123
+ const stmt = this.db.prepare(`
124
+ SELECT
125
+ mode,
126
+ COUNT(*) as total,
127
+ ROUND(AVG(CASE WHEN reward > 0 THEN 1.0 ELSE 0.0 END) * 100, 1) as win_rate
128
+ FROM battle_history
129
+ GROUP BY mode
130
+ `);
131
+ return stmt.all();
132
+ }
133
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Storage Abstraction Interface
3
+ *
4
+ * Defines the contract for all storage backends.
5
+ * Implementations: SqliteStorage, BetterSqliteStorage, MemoryStorage
6
+ */
7
+
8
+ /**
9
+ * Battle record
10
+ * @typedef {Object} BattleRecord
11
+ * @property {'train'|'test'} mode - Battle mode
12
+ * @property {number} handA - AI's action
13
+ * @property {number} handB - Opponent's action
14
+ * @property {number} reward - Reward (-1, 0, 1)
15
+ * @property {string} createdAt - ISO timestamp
16
+ */
17
+
18
+ /**
19
+ * Q-table record
20
+ * @typedef {Object} QTableRecord
21
+ * @property {string} state - State key
22
+ * @property {number} action - Action
23
+ * @property {number} qValue - Q-value
24
+ */
25
+
26
+ /**
27
+ * Performance stat by mode
28
+ * @typedef {Object} PerformanceStat
29
+ * @property {string} mode - 'train' or 'test'
30
+ * @property {number} total - Total battles
31
+ * @property {number} winRate - Win rate percentage
32
+ */
33
+
34
+ /**
35
+ * Abstract Storage Interface
36
+ * All storage backends must implement these methods.
37
+ */
38
+ export class Storage {
39
+ /**
40
+ * Initialize the storage (create tables, connections, etc.)
41
+ * @returns {Promise<void>}
42
+ */
43
+ async initialize() {
44
+ throw new Error('initialize() must be implemented');
45
+ }
46
+
47
+ /**
48
+ * Close the storage connection
49
+ * @returns {Promise<void>}
50
+ */
51
+ async close() {
52
+ throw new Error('close() must be implemented');
53
+ }
54
+
55
+ /**
56
+ * Get a setting value
57
+ * @param {string} key - Setting key
58
+ * @returns {Promise<{key: string, value: string}|null>}
59
+ */
60
+ async getSetting(key) {
61
+ throw new Error('getSetting() must be implemented');
62
+ }
63
+
64
+ /**
65
+ * Set a setting value
66
+ * @param {string} key - Setting key
67
+ * @param {string|number} value - Setting value
68
+ * @returns {Promise<void>}
69
+ */
70
+ async setSetting(key, value) {
71
+ throw new Error('setSetting() must be implemented');
72
+ }
73
+
74
+ /**
75
+ * Get all Q-table records
76
+ * @returns {Promise<Array<QTableRecord>>}
77
+ */
78
+ async getQTable() {
79
+ throw new Error('getQTable() must be implemented');
80
+ }
81
+
82
+ /**
83
+ * Update Q-value for state-action pair
84
+ * @param {string} state - State key
85
+ * @param {number} action - Action
86
+ * @param {number} qValue - Q-value
87
+ * @returns {Promise<void>}
88
+ */
89
+ async updateQ(state, action, qValue) {
90
+ throw new Error('updateQ() must be implemented');
91
+ }
92
+
93
+ /**
94
+ * Add a battle record
95
+ * @param {BattleRecord} record - Battle record
96
+ * @returns {Promise<void>}
97
+ */
98
+ async addBattle(record) {
99
+ throw new Error('addBattle() must be implemented');
100
+ }
101
+
102
+ /**
103
+ * Get today's battle count
104
+ * @returns {Promise<number>}
105
+ */
106
+ async getTodayBattleCount() {
107
+ throw new Error('getTodayBattleCount() must be implemented');
108
+ }
109
+
110
+ /**
111
+ * Get performance statistics by mode
112
+ * @returns {Promise<Array<PerformanceStat>>}
113
+ */
114
+ async getPerformanceStats() {
115
+ throw new Error('getPerformanceStats() must be implemented');
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Create storage instance by type
121
+ * @param {'sqlite'|'better-sqlite'|'memory'} type - Storage type
122
+ * @param {Object} options - Storage options
123
+ * @returns {Promise<Storage>} Initialized storage instance
124
+ */
125
+ export async function createStorage(type, options = {}) {
126
+ let storage;
127
+
128
+ switch (type) {
129
+ case 'sqlite':
130
+ const { SqliteStorage } = await import('./sqlite.js');
131
+ storage = new SqliteStorage(options.path || './lsji.db');
132
+ break;
133
+ case 'better-sqlite':
134
+ const { BetterSqliteStorage } = await import('./better-sqlite.js');
135
+ storage = new BetterSqliteStorage(options.path || './lsji.db');
136
+ break;
137
+ case 'memory':
138
+ default:
139
+ const { MemoryStorage } = await import('./memory.js');
140
+ storage = new MemoryStorage();
141
+ break;
142
+ }
143
+
144
+ await storage.initialize();
145
+ return storage;
146
+ }