appback-ai-agent 1.0.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/.env.example +21 -0
- package/README.md +119 -0
- package/bin/cli.js +74 -0
- package/package.json +43 -0
- package/src/adapters/gc/GcAdapter.js +427 -0
- package/src/adapters/gc/GcApiClient.js +69 -0
- package/src/adapters/gc/GcEquipmentManager.js +144 -0
- package/src/adapters/gc/GcFeatureBuilder.js +370 -0
- package/src/adapters/gc/GcSocketClient.js +73 -0
- package/src/adapters/gc/GcStrategyEngine.js +103 -0
- package/src/adapters/gc/config.js +16 -0
- package/src/core/AgentManager.js +59 -0
- package/src/core/BaseGameAdapter.js +23 -0
- package/src/core/BaseModelProvider.js +17 -0
- package/src/core/DataCollector.js +48 -0
- package/src/core/EventBus.js +10 -0
- package/src/core/HealthMonitor.js +69 -0
- package/src/core/ModelRegistry.js +77 -0
- package/src/core/Scheduler.js +40 -0
- package/src/core/TrainingRunner.js +70 -0
- package/src/data/exporters/TrainingExporter.js +78 -0
- package/src/data/storage/SqliteStore.js +172 -0
- package/src/index.js +109 -0
- package/src/models/providers/OnnxProvider.js +78 -0
- package/src/models/providers/RuleBasedProvider.js +18 -0
- package/src/utils/logger.js +20 -0
- package/src/utils/metrics.js +96 -0
- package/src/utils/retry.js +21 -0
- package/training/__init__.py +0 -0
- package/training/models/__init__.py +0 -0
- package/training/models/gc_strategy_net.py +46 -0
- package/training/requirements.txt +6 -0
- package/training/train_gc_model.py +226 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const Database = require('better-sqlite3')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { createLogger } = require('../../utils/logger')
|
|
4
|
+
const log = createLogger('sqlite')
|
|
5
|
+
|
|
6
|
+
class SqliteStore {
|
|
7
|
+
constructor(dataDir) {
|
|
8
|
+
const dbPath = path.join(dataDir || './data', 'agent.db')
|
|
9
|
+
this.db = new Database(dbPath)
|
|
10
|
+
this.db.pragma('journal_mode = WAL')
|
|
11
|
+
this._initSchema()
|
|
12
|
+
log.info(`SQLite database: ${dbPath}`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
_initSchema() {
|
|
16
|
+
this.db.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS agent_identity (
|
|
18
|
+
game TEXT PRIMARY KEY,
|
|
19
|
+
agent_id TEXT NOT NULL,
|
|
20
|
+
api_token TEXT NOT NULL,
|
|
21
|
+
name TEXT,
|
|
22
|
+
registered_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE TABLE IF NOT EXISTS game_sessions (
|
|
26
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
27
|
+
game TEXT NOT NULL,
|
|
28
|
+
game_id TEXT NOT NULL,
|
|
29
|
+
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
30
|
+
ended_at DATETIME,
|
|
31
|
+
my_slot INTEGER,
|
|
32
|
+
result TEXT,
|
|
33
|
+
model_version TEXT,
|
|
34
|
+
strategy_log TEXT
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS battle_ticks (
|
|
38
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
+
session_id INTEGER REFERENCES game_sessions(id),
|
|
40
|
+
tick INTEGER NOT NULL,
|
|
41
|
+
sub_tick INTEGER NOT NULL,
|
|
42
|
+
state TEXT NOT NULL,
|
|
43
|
+
my_features TEXT,
|
|
44
|
+
my_decision TEXT
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE TABLE IF NOT EXISTS training_samples (
|
|
48
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
49
|
+
model_type TEXT NOT NULL,
|
|
50
|
+
features BLOB NOT NULL,
|
|
51
|
+
label INTEGER NOT NULL,
|
|
52
|
+
reward REAL,
|
|
53
|
+
session_id INTEGER REFERENCES game_sessions(id),
|
|
54
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE TABLE IF NOT EXISTS model_metrics (
|
|
58
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
+
model_key TEXT NOT NULL,
|
|
60
|
+
version INTEGER NOT NULL,
|
|
61
|
+
games_played INTEGER DEFAULT 0,
|
|
62
|
+
avg_rank REAL,
|
|
63
|
+
avg_score REAL,
|
|
64
|
+
win_rate REAL,
|
|
65
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_ticks_session ON battle_ticks(session_id);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_game ON game_sessions(game, game_id);
|
|
70
|
+
`)
|
|
71
|
+
log.info('Schema initialized')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Identity ---
|
|
75
|
+
saveIdentity(game, agentId, apiToken, name) {
|
|
76
|
+
this.db.prepare(`
|
|
77
|
+
INSERT OR REPLACE INTO agent_identity (game, agent_id, api_token, name)
|
|
78
|
+
VALUES (?, ?, ?, ?)
|
|
79
|
+
`).run(game, agentId, apiToken, name)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getIdentity(game) {
|
|
83
|
+
return this.db.prepare('SELECT * FROM agent_identity WHERE game = ?').get(game)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Game Sessions ---
|
|
87
|
+
startSession(game, gameId, mySlot) {
|
|
88
|
+
const info = this.db.prepare(`
|
|
89
|
+
INSERT INTO game_sessions (game, game_id, my_slot) VALUES (?, ?, ?)
|
|
90
|
+
`).run(game, gameId, mySlot)
|
|
91
|
+
return info.lastInsertRowid
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
endSession(sessionId, result, strategyLog) {
|
|
95
|
+
this.db.prepare(`
|
|
96
|
+
UPDATE game_sessions SET ended_at = CURRENT_TIMESTAMP, result = ?, strategy_log = ?
|
|
97
|
+
WHERE id = ?
|
|
98
|
+
`).run(JSON.stringify(result), JSON.stringify(strategyLog), sessionId)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- Battle Ticks ---
|
|
102
|
+
recordTick(sessionId, tick, subTick, state, features, decision) {
|
|
103
|
+
this.db.prepare(`
|
|
104
|
+
INSERT INTO battle_ticks (session_id, tick, sub_tick, state, my_features, my_decision)
|
|
105
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
106
|
+
`).run(
|
|
107
|
+
sessionId, tick, subTick,
|
|
108
|
+
JSON.stringify(state),
|
|
109
|
+
features ? JSON.stringify(features) : null,
|
|
110
|
+
decision ? JSON.stringify(decision) : null
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Batch insert for performance
|
|
115
|
+
recordTickBatch(rows) {
|
|
116
|
+
const stmt = this.db.prepare(`
|
|
117
|
+
INSERT INTO battle_ticks (session_id, tick, sub_tick, state, my_features, my_decision)
|
|
118
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
119
|
+
`)
|
|
120
|
+
const tx = this.db.transaction((items) => {
|
|
121
|
+
for (const r of items) {
|
|
122
|
+
stmt.run(
|
|
123
|
+
r.sessionId, r.tick, r.subTick,
|
|
124
|
+
JSON.stringify(r.state),
|
|
125
|
+
r.features ? JSON.stringify(r.features) : null,
|
|
126
|
+
r.decision ? JSON.stringify(r.decision) : null
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
tx(rows)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- Training Samples ---
|
|
134
|
+
saveSample(modelType, features, label, reward, sessionId) {
|
|
135
|
+
const buf = Buffer.from(Float32Array.from(features).buffer)
|
|
136
|
+
this.db.prepare(`
|
|
137
|
+
INSERT INTO training_samples (model_type, features, label, reward, session_id)
|
|
138
|
+
VALUES (?, ?, ?, ?, ?)
|
|
139
|
+
`).run(modelType, buf, label, reward, sessionId)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getSampleCount(modelType) {
|
|
143
|
+
const row = this.db.prepare('SELECT COUNT(*) as cnt FROM training_samples WHERE model_type = ?').get(modelType)
|
|
144
|
+
return row.cnt
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- Model Metrics ---
|
|
148
|
+
recordMetrics(modelKey, version, gamesPlayed, avgRank, avgScore, winRate) {
|
|
149
|
+
this.db.prepare(`
|
|
150
|
+
INSERT INTO model_metrics (model_key, version, games_played, avg_rank, avg_score, win_rate)
|
|
151
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
152
|
+
`).run(modelKey, version, gamesPlayed, avgRank, avgScore, winRate)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- Stats ---
|
|
156
|
+
getSessionCount(game) {
|
|
157
|
+
const row = this.db.prepare('SELECT COUNT(*) as cnt FROM game_sessions WHERE game = ?').get(game)
|
|
158
|
+
return row.cnt
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getRecentSessions(game, limit = 10) {
|
|
162
|
+
return this.db.prepare(`
|
|
163
|
+
SELECT * FROM game_sessions WHERE game = ? ORDER BY id DESC LIMIT ?
|
|
164
|
+
`).all(game, limit)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
close() {
|
|
168
|
+
this.db.close()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = SqliteStore
|
package/src/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// CLI(bin/cli.js)에서 이미 로드한 경우 스킵
|
|
2
|
+
if (!process.env._PKG_ROOT) require('dotenv').config()
|
|
3
|
+
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const AgentManager = require('./core/AgentManager')
|
|
6
|
+
const EventBus = require('./core/EventBus')
|
|
7
|
+
const ModelRegistry = require('./core/ModelRegistry')
|
|
8
|
+
const DataCollector = require('./core/DataCollector')
|
|
9
|
+
const TrainingRunner = require('./core/TrainingRunner')
|
|
10
|
+
const HealthMonitor = require('./core/HealthMonitor')
|
|
11
|
+
const TrainingExporter = require('./data/exporters/TrainingExporter')
|
|
12
|
+
const SqliteStore = require('./data/storage/SqliteStore')
|
|
13
|
+
const Metrics = require('./utils/metrics')
|
|
14
|
+
const GcAdapter = require('./adapters/gc/GcAdapter')
|
|
15
|
+
const gcConfig = require('./adapters/gc/config')
|
|
16
|
+
const { createLogger } = require('./utils/logger')
|
|
17
|
+
|
|
18
|
+
const log = createLogger('main')
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
log.info('appback-ai-agent v0.4.0 starting...')
|
|
22
|
+
|
|
23
|
+
const eventBus = new EventBus()
|
|
24
|
+
const modelDir = process.env.MODEL_DIR || './models'
|
|
25
|
+
const dataDir = process.env.DATA_DIR || './data'
|
|
26
|
+
const autoTrainAfter = parseInt(process.env.AUTO_TRAIN_AFTER_GAMES || '50')
|
|
27
|
+
const healthPort = parseInt(process.env.HEALTH_PORT || '9090')
|
|
28
|
+
|
|
29
|
+
// Data layer
|
|
30
|
+
const store = new SqliteStore(dataDir)
|
|
31
|
+
const dataCollector = new DataCollector(store)
|
|
32
|
+
const modelRegistry = new ModelRegistry(modelDir)
|
|
33
|
+
const metrics = new Metrics(store)
|
|
34
|
+
const exporter = new TrainingExporter(store)
|
|
35
|
+
const pkgRoot = process.env._PKG_ROOT || path.resolve(__dirname, '..')
|
|
36
|
+
const trainingDataDir = path.join(pkgRoot, 'training', 'data', 'raw')
|
|
37
|
+
const trainer = new TrainingRunner({
|
|
38
|
+
dataDir: trainingDataDir,
|
|
39
|
+
outputDir: `${modelDir}/gc`,
|
|
40
|
+
autoTrainAfterGames: autoTrainAfter,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// Load historical metrics
|
|
44
|
+
metrics.load('claw-clash')
|
|
45
|
+
|
|
46
|
+
const config = {
|
|
47
|
+
discoveryIntervalSec: parseInt(process.env.GAME_DISCOVERY_INTERVAL_SEC || '30'),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const manager = new AgentManager(config)
|
|
51
|
+
|
|
52
|
+
// Register gc adapter with metrics
|
|
53
|
+
const gc = new GcAdapter({
|
|
54
|
+
config: gcConfig,
|
|
55
|
+
modelRegistry,
|
|
56
|
+
dataCollector,
|
|
57
|
+
eventBus,
|
|
58
|
+
metrics,
|
|
59
|
+
})
|
|
60
|
+
manager.registerAdapter(gc)
|
|
61
|
+
|
|
62
|
+
// Health monitor
|
|
63
|
+
const health = new HealthMonitor(healthPort, metrics, manager.adapters)
|
|
64
|
+
health.start()
|
|
65
|
+
|
|
66
|
+
// Graceful shutdown
|
|
67
|
+
const shutdown = async () => {
|
|
68
|
+
log.info('Shutting down...')
|
|
69
|
+
health.stop()
|
|
70
|
+
await manager.stop()
|
|
71
|
+
store.close()
|
|
72
|
+
process.exit(0)
|
|
73
|
+
}
|
|
74
|
+
process.on('SIGINT', shutdown)
|
|
75
|
+
process.on('SIGTERM', shutdown)
|
|
76
|
+
|
|
77
|
+
// Start
|
|
78
|
+
await manager.start()
|
|
79
|
+
|
|
80
|
+
// Event handlers
|
|
81
|
+
eventBus.on('game_found', ({ game, gameId }) => {
|
|
82
|
+
log.info(`Game found: ${game} / ${gameId}`)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Auto-training pipeline after games
|
|
86
|
+
eventBus.on('game_ended', async ({ game, gameId }) => {
|
|
87
|
+
log.info(`Game completed: ${game} / ${gameId}`)
|
|
88
|
+
|
|
89
|
+
const totalGames = dataCollector.getSessionCount(game)
|
|
90
|
+
|
|
91
|
+
if (totalGames > 0 && totalGames % autoTrainAfter === 0 && !trainer.isRunning) {
|
|
92
|
+
log.info(`Auto-train threshold reached (${totalGames} games), starting pipeline...`)
|
|
93
|
+
|
|
94
|
+
const result = exporter.exportForTraining(game)
|
|
95
|
+
if (result) {
|
|
96
|
+
trainer.run(game).then(success => {
|
|
97
|
+
if (success) {
|
|
98
|
+
log.info('New model will be loaded automatically via hot-reload')
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
main().catch(err => {
|
|
107
|
+
log.error('Fatal error', err.message)
|
|
108
|
+
process.exit(1)
|
|
109
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const BaseModelProvider = require('../../core/BaseModelProvider')
|
|
2
|
+
const { createLogger } = require('../../utils/logger')
|
|
3
|
+
const log = createLogger('onnx')
|
|
4
|
+
|
|
5
|
+
let ort = null
|
|
6
|
+
try {
|
|
7
|
+
ort = require('onnxruntime-node')
|
|
8
|
+
} catch {
|
|
9
|
+
log.warn('onnxruntime-node not available, ONNX inference disabled')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class OnnxProvider extends BaseModelProvider {
|
|
13
|
+
constructor(name, config = {}) {
|
|
14
|
+
super(name, config)
|
|
15
|
+
this.session = null
|
|
16
|
+
this.featureDim = config.featureDim || 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async load(modelPath) {
|
|
20
|
+
if (!ort) throw new Error('onnxruntime-node not installed')
|
|
21
|
+
try {
|
|
22
|
+
this.session = await ort.InferenceSession.create(modelPath, {
|
|
23
|
+
executionProviders: ['cpu'],
|
|
24
|
+
})
|
|
25
|
+
this._loaded = true
|
|
26
|
+
log.info(`Model loaded: ${this.name} (${modelPath})`)
|
|
27
|
+
} catch (err) {
|
|
28
|
+
log.error(`Failed to load model: ${this.name}`, err.message)
|
|
29
|
+
throw err
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async infer(features) {
|
|
34
|
+
if (!this.session) return null
|
|
35
|
+
const start = Date.now()
|
|
36
|
+
try {
|
|
37
|
+
const tensor = new ort.Tensor(
|
|
38
|
+
'float32',
|
|
39
|
+
Float32Array.from(features),
|
|
40
|
+
[1, features.length]
|
|
41
|
+
)
|
|
42
|
+
const results = await this.session.run({ input: tensor })
|
|
43
|
+
const outputKey = Object.keys(results)[0]
|
|
44
|
+
const logits = Array.from(results[outputKey].data)
|
|
45
|
+
const idx = argmax(logits)
|
|
46
|
+
const probs = softmax(logits)
|
|
47
|
+
|
|
48
|
+
this._lastInferenceMs = Date.now() - start
|
|
49
|
+
return { logits, decision: idx, confidence: probs[idx], probs }
|
|
50
|
+
} catch (err) {
|
|
51
|
+
log.error(`Inference error: ${this.name}`, err.message)
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async unload() {
|
|
57
|
+
this.session = null
|
|
58
|
+
this._loaded = false
|
|
59
|
+
log.info(`Model unloaded: ${this.name}`)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function argmax(arr) {
|
|
64
|
+
let maxIdx = 0, maxVal = arr[0]
|
|
65
|
+
for (let i = 1; i < arr.length; i++) {
|
|
66
|
+
if (arr[i] > maxVal) { maxVal = arr[i]; maxIdx = i }
|
|
67
|
+
}
|
|
68
|
+
return maxIdx
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function softmax(arr) {
|
|
72
|
+
const max = Math.max(...arr)
|
|
73
|
+
const exp = arr.map(x => Math.exp(x - max))
|
|
74
|
+
const sum = exp.reduce((a, b) => a + b)
|
|
75
|
+
return exp.map(x => x / sum)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = OnnxProvider
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const BaseModelProvider = require('../../core/BaseModelProvider')
|
|
2
|
+
|
|
3
|
+
class RuleBasedProvider extends BaseModelProvider {
|
|
4
|
+
constructor() {
|
|
5
|
+
super('rule-based', {})
|
|
6
|
+
this._loaded = true
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async load() { this._loaded = true }
|
|
10
|
+
|
|
11
|
+
async infer(features) {
|
|
12
|
+
// Rule-based doesn't use feature vectors
|
|
13
|
+
// Strategy decisions are handled by GcStrategyEngine
|
|
14
|
+
return { decision: null, confidence: 0 }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = RuleBasedProvider
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }
|
|
2
|
+
const level = LEVELS[process.env.LOG_LEVEL || 'info'] || 1
|
|
3
|
+
|
|
4
|
+
function fmt(lvl, tag, msg, data) {
|
|
5
|
+
const ts = new Date().toISOString()
|
|
6
|
+
const base = `[${ts}] [${lvl.toUpperCase()}] [${tag}] ${msg}`
|
|
7
|
+
if (data !== undefined) console.log(base, typeof data === 'object' ? JSON.stringify(data) : data)
|
|
8
|
+
else console.log(base)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function createLogger(tag) {
|
|
12
|
+
return {
|
|
13
|
+
debug: (msg, data) => level <= 0 && fmt('debug', tag, msg, data),
|
|
14
|
+
info: (msg, data) => level <= 1 && fmt('info', tag, msg, data),
|
|
15
|
+
warn: (msg, data) => level <= 2 && fmt('warn', tag, msg, data),
|
|
16
|
+
error: (msg, data) => level <= 3 && fmt('error', tag, msg, data),
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { createLogger }
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const { createLogger } = require('./logger')
|
|
2
|
+
const log = createLogger('metrics')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Metrics — Tracks agent performance across games.
|
|
6
|
+
*/
|
|
7
|
+
class Metrics {
|
|
8
|
+
constructor(store) {
|
|
9
|
+
this.store = store
|
|
10
|
+
this._current = {
|
|
11
|
+
gamesPlayed: 0,
|
|
12
|
+
wins: 0,
|
|
13
|
+
podiums: 0, // top 3
|
|
14
|
+
totalScore: 0,
|
|
15
|
+
totalRank: 0,
|
|
16
|
+
kills: 0,
|
|
17
|
+
modelVersion: 'rule-based',
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
load(game) {
|
|
22
|
+
if (!this.store) return
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const sessions = this.store.db.prepare(`
|
|
26
|
+
SELECT result FROM game_sessions
|
|
27
|
+
WHERE game = ? AND result IS NOT NULL
|
|
28
|
+
`).all(game)
|
|
29
|
+
|
|
30
|
+
for (const s of sessions) {
|
|
31
|
+
const r = JSON.parse(s.result || '{}')
|
|
32
|
+
this._record(r)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
log.info(`Loaded metrics: ${this._current.gamesPlayed} games, ` +
|
|
36
|
+
`win rate ${this.winRate}%, avg rank ${this.avgRank}`)
|
|
37
|
+
} catch (err) {
|
|
38
|
+
log.warn('Failed to load metrics', err.message)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
record(result) {
|
|
43
|
+
this._record(result)
|
|
44
|
+
this._logSummary()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_record(result) {
|
|
48
|
+
if (!result) return
|
|
49
|
+
this._current.gamesPlayed++
|
|
50
|
+
if (result.placement === 1) this._current.wins++
|
|
51
|
+
if (result.placement <= 3) this._current.podiums++
|
|
52
|
+
this._current.totalScore += result.score || 0
|
|
53
|
+
this._current.totalRank += result.placement || 8
|
|
54
|
+
this._current.kills += result.kills || 0
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get gamesPlayed() { return this._current.gamesPlayed }
|
|
58
|
+
get winRate() {
|
|
59
|
+
if (this._current.gamesPlayed === 0) return '0.0'
|
|
60
|
+
return (this._current.wins / this._current.gamesPlayed * 100).toFixed(1)
|
|
61
|
+
}
|
|
62
|
+
get podiumRate() {
|
|
63
|
+
if (this._current.gamesPlayed === 0) return '0.0'
|
|
64
|
+
return (this._current.podiums / this._current.gamesPlayed * 100).toFixed(1)
|
|
65
|
+
}
|
|
66
|
+
get avgRank() {
|
|
67
|
+
if (this._current.gamesPlayed === 0) return '0.0'
|
|
68
|
+
return (this._current.totalRank / this._current.gamesPlayed).toFixed(2)
|
|
69
|
+
}
|
|
70
|
+
get avgScore() {
|
|
71
|
+
if (this._current.gamesPlayed === 0) return '0'
|
|
72
|
+
return (this._current.totalScore / this._current.gamesPlayed).toFixed(0)
|
|
73
|
+
}
|
|
74
|
+
get totalKills() { return this._current.kills }
|
|
75
|
+
|
|
76
|
+
_logSummary() {
|
|
77
|
+
log.info(
|
|
78
|
+
`[${this._current.gamesPlayed} games] ` +
|
|
79
|
+
`win: ${this.winRate}% | top3: ${this.podiumRate}% | ` +
|
|
80
|
+
`avgRank: ${this.avgRank} | avgScore: ${this.avgScore} | kills: ${this.totalKills}`
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
toJSON() {
|
|
85
|
+
return {
|
|
86
|
+
gamesPlayed: this.gamesPlayed,
|
|
87
|
+
winRate: this.winRate + '%',
|
|
88
|
+
podiumRate: this.podiumRate + '%',
|
|
89
|
+
avgRank: this.avgRank,
|
|
90
|
+
avgScore: this.avgScore,
|
|
91
|
+
totalKills: this.totalKills,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = Metrics
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { createLogger } = require('./logger')
|
|
2
|
+
const log = createLogger('retry')
|
|
3
|
+
|
|
4
|
+
async function retry(fn, { maxAttempts = 3, delayMs = 1000, backoff = 2 } = {}) {
|
|
5
|
+
let lastErr
|
|
6
|
+
for (let i = 1; i <= maxAttempts; i++) {
|
|
7
|
+
try {
|
|
8
|
+
return await fn()
|
|
9
|
+
} catch (err) {
|
|
10
|
+
lastErr = err
|
|
11
|
+
if (i < maxAttempts) {
|
|
12
|
+
const wait = delayMs * Math.pow(backoff, i - 1)
|
|
13
|
+
log.warn(`Attempt ${i}/${maxAttempts} failed, retrying in ${wait}ms`, err.message)
|
|
14
|
+
await new Promise(r => setTimeout(r, wait))
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
throw lastErr
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { retry }
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GC Strategy Network - Predicts optimal strategy from game state features.
|
|
3
|
+
|
|
4
|
+
Input: 120-dim move feature vector (same as featureBuilder)
|
|
5
|
+
Output: 7 classes (3 modes × 2 target priorities + 1 flee flag)
|
|
6
|
+
|
|
7
|
+
Simplified output mapping:
|
|
8
|
+
0: aggressive + nearest
|
|
9
|
+
1: aggressive + lowest_hp
|
|
10
|
+
2: balanced + nearest
|
|
11
|
+
3: balanced + lowest_hp
|
|
12
|
+
4: defensive + nearest
|
|
13
|
+
5: defensive + lowest_hp
|
|
14
|
+
6: flee (defensive + high flee_threshold)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import torch
|
|
18
|
+
import torch.nn as nn
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GcStrategyNet(nn.Module):
|
|
22
|
+
def __init__(self, input_dim=120, hidden1=64, hidden2=32, output_dim=7):
|
|
23
|
+
super().__init__()
|
|
24
|
+
self.net = nn.Sequential(
|
|
25
|
+
nn.Linear(input_dim, hidden1),
|
|
26
|
+
nn.ReLU(),
|
|
27
|
+
nn.Dropout(0.2),
|
|
28
|
+
nn.Linear(hidden1, hidden2),
|
|
29
|
+
nn.ReLU(),
|
|
30
|
+
nn.Dropout(0.1),
|
|
31
|
+
nn.Linear(hidden2, output_dim),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def forward(self, x):
|
|
35
|
+
return self.net(x)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
STRATEGY_MAP = [
|
|
39
|
+
{"mode": "aggressive", "target_priority": "nearest", "flee_threshold": 10},
|
|
40
|
+
{"mode": "aggressive", "target_priority": "lowest_hp", "flee_threshold": 10},
|
|
41
|
+
{"mode": "balanced", "target_priority": "nearest", "flee_threshold": 15},
|
|
42
|
+
{"mode": "balanced", "target_priority": "lowest_hp", "flee_threshold": 15},
|
|
43
|
+
{"mode": "defensive", "target_priority": "nearest", "flee_threshold": 20},
|
|
44
|
+
{"mode": "defensive", "target_priority": "lowest_hp", "flee_threshold": 20},
|
|
45
|
+
{"mode": "defensive", "target_priority": "nearest", "flee_threshold": 30},
|
|
46
|
+
]
|