nirkyy commited on
Commit
1f7dedd
·
verified ·
1 Parent(s): 25f5b9c

Update index.js

Browse files
Files changed (1) hide show
  1. index.js +197 -167
index.js CHANGED
@@ -1,4 +1,3 @@
1
-
2
  const express = require('express');
3
  const { spawn } = require('child_process');
4
  const { Chess } = require('chess.js');
@@ -7,69 +6,95 @@ const app = express();
7
  const PORT = process.env.PORT || 7860;
8
  const STOCKFISH_PATH = '/usr/games/stockfish';
9
 
10
- let engine;
11
- let isEngineBusy = false;
12
- const requestQueue = [];
13
- let currentRequestResolver = null;
 
14
 
15
  app.use(express.json());
16
 
17
- function initializeEngine() {
 
 
 
 
 
 
 
 
 
18
  return new Promise((resolve, reject) => {
19
  try {
20
- engine = spawn(STOCKFISH_PATH);
21
  } catch (error) {
22
- return reject(new Error(`Failed to spawn Stockfish. Make sure the binary is at '${STOCKFISH_PATH}' and has execute permissions. Details: ${error.message}`));
23
  }
24
 
25
- const startupTimeout = setTimeout(() => {
26
- reject(new Error('Stockfish startup timed out. The engine failed to send "readyok".'));
27
  }, 10000);
28
 
29
- engine.on('error', (err) => {
30
- clearTimeout(startupTimeout);
31
- reject(new Error(`Failed to start Stockfish process: ${err.message}`));
32
  });
33
 
34
- engine.on('close', (code) => {
35
- console.error(`Stockfish process exited with code ${code}. The application will now exit.`);
36
  process.exit(1);
37
  });
38
 
39
- engine.stderr.on('data', (data) => {
40
- console.error(`Stockfish stderr: ${data.toString()}`);
41
  });
42
 
43
- engine.stdout.on('data', (data) => {
44
  const output = data.toString();
45
-
46
  if (output.includes('readyok')) {
47
- console.log('Stockfish engine is ready.');
48
- clearTimeout(startupTimeout);
49
 
50
- engine.stdout.removeAllListeners('data');
51
 
52
- engine.stdout.on('data', (data) => {
53
- const lines = data.toString().split('\n');
54
- for (const line of lines) {
55
- if (line.startsWith('bestmove')) {
56
- const parts = line.split(' ');
57
- const bestMove = parts[1];
58
-
59
- if (currentRequestResolver) {
60
- if (!bestMove || bestMove === '(none)') {
61
- currentRequestResolver.reject(new Error('No valid move found'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  } else {
63
- const from = bestMove.slice(0, 2);
64
- const to = bestMove.slice(2, 4);
65
- const promotion = bestMove.length === 5 ? bestMove[4] : null;
66
- currentRequestResolver.resolve({ bestMove, from, to, promotion });
 
 
67
  }
68
  }
69
 
70
- isEngineBusy = false;
71
- currentRequestResolver = null;
72
- processQueue();
73
  return;
74
  }
75
  }
@@ -79,51 +104,41 @@ function initializeEngine() {
79
  }
80
  });
81
 
82
- engine.stdin.write('uci\n');
83
- engine.stdin.write('isready\n');
84
  });
85
  }
86
 
87
- function processQueue() {
88
- if (isEngineBusy || requestQueue.length === 0) {
89
  return;
90
  }
91
-
92
- isEngineBusy = true;
93
- const request = requestQueue.shift();
94
- currentRequestResolver = request;
95
-
96
- const moveTimeout = setTimeout(() => {
97
- if (currentRequestResolver === request) {
98
- request.reject(new Error('Engine move calculation timed out'));
99
- isEngineBusy = false;
100
- currentRequestResolver = null;
101
- processQueue();
102
  }
103
  }, 15000);
104
 
105
- request.resolve = (value) => {
106
- clearTimeout(moveTimeout);
107
- request.originalResolve(value);
108
- };
109
-
110
- request.reject = (reason) => {
111
- clearTimeout(moveTimeout);
112
- request.originalReject(reason);
113
- };
114
 
115
- engine.stdin.write(`position fen ${request.fen}\n`);
116
- engine.stdin.write('go movetime 2000\n');
 
117
  }
118
 
119
- function getBestMoveFromEngine(fen) {
120
  return new Promise((resolve, reject) => {
121
- requestQueue.push({
122
- fen,
123
- originalResolve: resolve,
124
- originalReject: reject,
125
- });
126
- processQueue();
127
  });
128
  }
129
 
@@ -131,86 +146,94 @@ app.use((req, res, next) => {
131
  res.setHeader('Access-Control-Allow-Origin', '*');
132
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
133
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
134
- if (req.method === 'OPTIONS') {
135
- return res.sendStatus(204);
136
- }
137
  next();
138
  });
139
 
140
  app.get('/', (req, res) => {
141
  const baseUrl = `${req.protocol}://${req.get('host')}`;
 
142
  const doc = `
143
- STOCKFISH CHESS API DOCUMENTATION
144
- =================================
145
 
146
- This API provides access to the Stockfish chess engine for getting the best move and analyzing games.
147
 
148
  ---------------------------------
149
  ENDPOINT: GET /chessbot
150
  ---------------------------------
151
- Calculates the best move for a given FEN position.
152
 
153
- METHOD: GET
154
 
155
  URL: ${baseUrl}/chessbot
156
 
157
- QUERY PARAMETERS:
158
- - fen (string, required): The FEN string of the chess position.
159
 
160
- EXAMPLE REQUEST:
161
  GET ${baseUrl}/chessbot?fen=rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2
162
 
163
- EXAMPLE RESPONSE (SUCCESS):
164
  {
165
  "bestMove": "g1f3",
166
  "from": "g1",
167
  "to": "f3",
168
- "promotion": null
 
169
  }
 
 
170
 
171
- EXAMPLE RESPONSE (ERROR):
172
  {
173
- "error": "missing fen"
174
  }
175
 
176
  ---------------------------------
177
  ENDPOINT: POST /analisis
178
  ---------------------------------
179
- Analyzes a full game from a PGN string. It compares the move played in the game with the best move suggested by the engine for each position.
180
 
181
- METHOD: POST
182
 
183
  URL: ${baseUrl}/analisis
184
 
185
- REQUEST BODY:
186
  - Content-Type: application/json
187
- - Body: { "pgn": "<Your-PGN-String>" }
188
 
189
- EXAMPLE PAYLOAD:
190
  {
191
- "pgn": "[Event \\"Live Chess\\"]\\n[Site \\"Chess.com\\"]\\n[Date \\"2025.09.28\\"]\\n[Round \\"?\\"]\\n[White \\"Puru-Jawa\\"]\\n[Black \\"saahil905\\"]\\n[Result \\"1-0\\"]\\n\\n1. e4 Nc6 2. Nf3 f6 3. e5 Nxe5 4. Qe2 b5 5. Nxe5 fxe5 6. Qxe5 d6 7. Qxb5+ Qd7 8. d3 c6 9. Qa4 Bb7 10. Nc3 c5 11. Be3 Bc6 12. Qh4 Qb7 13. b3 Bxg2 14. Bxg2 Qxg2 15. Ke2 h6 16. Rhg1 g5 17. Qa4+ 1-0"
192
  }
193
 
194
- EXAMPLE RESPONSE (SUCCESS):
195
- [
196
- {
197
- "moveNumber": "1.",
198
- "playedMove": "e4",
199
- "bestMove": "e4",
200
- "fenBeforeMove": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
201
- },
202
- {
203
- "moveNumber": "1...",
204
- "playedMove": "Nc6",
205
- "bestMove": "e5",
206
- "fenBeforeMove": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1"
207
- },
208
- ... (and so on for every move in the PGN)
209
- ]
210
-
211
- EXAMPLE RESPONSE (ERROR):
 
 
 
 
 
212
  {
213
- "error": "Invalid PGN provided"
 
214
  }
215
  `;
216
  res.setHeader('Content-Type', 'text/plain');
@@ -219,13 +242,10 @@ EXAMPLE RESPONSE (ERROR):
219
 
220
  app.get('/chessbot', async (req, res) => {
221
  const fen = req.query.fen;
222
- if (!fen) {
223
- return res.status(400).json({ error: 'missing fen' });
224
- }
225
-
226
  try {
227
- const move = await getBestMoveFromEngine(fen);
228
- res.json(move);
229
  } catch (e) {
230
  res.status(500).json({ error: e.message });
231
  }
@@ -233,74 +253,84 @@ app.get('/chessbot', async (req, res) => {
233
 
234
  app.post('/analisis', async (req, res) => {
235
  const { pgn } = req.body;
236
- if (!pgn) {
237
- return res.status(400).json({ error: 'missing pgn in request body' });
238
- }
239
 
240
- const chess = new Chess();
241
  try {
242
- if (typeof chess.loadPgn === 'function') {
243
- chess.loadPgn(pgn);
244
- } else {
245
- chess.load_pgn(pgn);
246
- }
247
  } catch (e) {
248
- return res.status(400).json({ error: 'Invalid PGN provided', details: e.message });
249
  }
250
 
251
- const history = chess.history({ verbose: true });
252
- if (history.length === 0) {
253
- return res.status(400).json({ error: 'PGN contains no valid moves' });
254
- }
 
 
 
255
 
256
- const analysisResults = [];
257
  try {
258
- const game = new Chess();
259
- for (const move of history) {
260
- const fenBeforeMove = game.fen();
261
- const engineResponse = await getBestMoveFromEngine(fenBeforeMove);
262
 
263
- let bestMoveSan = engineResponse.bestMove;
264
- try {
265
- const tempGame = new Chess(fenBeforeMove);
266
- const moveResult = tempGame.move({
267
- from: engineResponse.from,
268
- to: engineResponse.to,
269
- promotion: engineResponse.promotion
270
- });
271
- if (moveResult) {
272
- bestMoveSan = moveResult.san;
273
- }
274
- } catch (e) {
275
- // Ignore conversion error, use UCI move
276
- }
277
 
278
- analysisResults.push({
279
- moveNumber: `${game.moveNumber()}${game.turn() === 'w' ? '.' : '...'}`,
280
- playedMove: move.san,
281
- bestMove: bestMoveSan,
282
- fenBeforeMove: fenBeforeMove,
283
- });
284
- game.move(move.san);
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  }
286
- res.json(analysisResults);
 
 
 
287
  } catch (e) {
288
- res.status(500).json({ error: 'Analysis failed', details: e.message });
 
 
289
  }
290
  });
291
 
292
- async function startServer() {
293
  try {
294
- console.log('Initializing Stockfish engine...');
295
- await initializeEngine();
296
  app.listen(PORT, () => {
297
- console.log(`Server is running on port ${PORT}`);
298
- console.log(`API documentation available at http://localhost:${PORT}/`);
299
  });
300
  } catch (error) {
301
- console.error('Failed to start the application:', error.message);
302
  process.exit(1);
303
  }
304
  }
305
 
306
- startServer();
 
 
1
  const express = require('express');
2
  const { spawn } = require('child_process');
3
  const { Chess } = require('chess.js');
 
6
  const PORT = process.env.PORT || 7860;
7
  const STOCKFISH_PATH = '/usr/games/stockfish';
8
 
9
+ let mesin;
10
+ let isMesinSibuk = false;
11
+ const antrianRequest = [];
12
+ let resolverRequestSekarang = null;
13
+ let analisisSekarang = { score: null, bestMove: null };
14
 
15
  app.use(express.json());
16
 
17
+ function klasifikasiLangkah(rugiCentipawn) {
18
+ if (rugiCentipawn <= 15) return 'Terbaik';
19
+ if (rugiCentipawn <= 50) return 'Luar Biasa';
20
+ if (rugiCentipawn <= 100) return 'Bagus';
21
+ if (rugiCentipawn <= 200) return 'Kurang Akurat';
22
+ if (rugiCentipawn <= 350) return 'Kesalahan';
23
+ return 'Blunder';
24
+ }
25
+
26
+ function siapkanMesin() {
27
  return new Promise((resolve, reject) => {
28
  try {
29
+ mesin = spawn(STOCKFISH_PATH);
30
  } catch (error) {
31
+ return reject(new Error(`Gagal menjalankan Stockfish. Pastikan file ada di '${STOCKFISH_PATH}' dan bisa dieksekusi. Detail: ${error.message}`));
32
  }
33
 
34
+ const waktuTungguStartup = setTimeout(() => {
35
+ reject(new Error('Waktu startup Stockfish habis. Mesin gagal mengirim "readyok".'));
36
  }, 10000);
37
 
38
+ mesin.on('error', (err) => {
39
+ clearTimeout(waktuTungguStartup);
40
+ reject(new Error(`Gagal memulai proses Stockfish: ${err.message}`));
41
  });
42
 
43
+ mesin.on('close', (code) => {
44
+ console.error(`Proses Stockfish berhenti dengan kode ${code}. Aplikasi akan ditutup.`);
45
  process.exit(1);
46
  });
47
 
48
+ mesin.stderr.on('data', (data) => {
49
+ console.error(`Pesan error dari Stockfish: ${data.toString()}`);
50
  });
51
 
52
+ mesin.stdout.on('data', (data) => {
53
  const output = data.toString();
 
54
  if (output.includes('readyok')) {
55
+ console.log('Mesin Stockfish sudah siap.');
56
+ clearTimeout(waktuTungguStartup);
57
 
58
+ mesin.stdout.removeAllListeners('data');
59
 
60
+ mesin.stdout.on('data', (data) => {
61
+ const baris = data.toString().split('\n');
62
+ for (const teks of baris) {
63
+ if (teks.startsWith('info') && teks.includes('score')) {
64
+ const bagian = teks.split(' ');
65
+ const indeksSkor = bagian.indexOf('score');
66
+ if (indeksSkor !== -1) {
67
+ const tipeSkor = bagian[indeksSkor + 1];
68
+ const nilaiSkor = parseInt(bagian[indeksSkor + 2], 10);
69
+ if (tipeSkor === 'cp') {
70
+ analisisSekarang.score = nilaiSkor;
71
+ } else if (tipeSkor === 'mate') {
72
+ analisisSekarang.score = (nilaiSkor > 0 ? 20000 - nilaiSkor : -20000 - nilaiSkor);
73
+ }
74
+ }
75
+ }
76
+
77
+ if (teks.startsWith('bestmove')) {
78
+ const bagian = teks.split(' ');
79
+ const langkahTerbaik = bagian[1];
80
+ analisisSekarang.bestMove = langkahTerbaik;
81
+
82
+ if (resolverRequestSekarang) {
83
+ if (!langkahTerbaik || langkahTerbaik === '(none)') {
84
+ resolverRequestSekarang.reject(new Error('Tidak ditemukan langkah yang valid'));
85
  } else {
86
+ const dari = langkahTerbaik.slice(0, 2);
87
+ const ke = langkahTerbaik.slice(2, 4);
88
+ const promosi = langkahTerbaik.length === 5 ? langkahTerbaik[4] : null;
89
+ resolverRequestSekarang.resolve({
90
+ bestMove: langkahTerbaik, dari, ke, promosi, score: analisisSekarang.score
91
+ });
92
  }
93
  }
94
 
95
+ isMesinSibuk = false;
96
+ resolverRequestSekarang = null;
97
+ prosesAntrian();
98
  return;
99
  }
100
  }
 
104
  }
105
  });
106
 
107
+ mesin.stdin.write('uci\n');
108
+ mesin.stdin.write('isready\n');
109
  });
110
  }
111
 
112
+ function prosesAntrian() {
113
+ if (isMesinSibuk || antrianRequest.length === 0) {
114
  return;
115
  }
116
+ isMesinSibuk = true;
117
+ const request = antrianRequest.shift();
118
+ resolverRequestSekarang = request;
119
+ analisisSekarang = { score: null, bestMove: null };
120
+
121
+ const batasWaktuLangkah = setTimeout(() => {
122
+ if (resolverRequestSekarang === request) {
123
+ request.reject(new Error('Waktu kalkulasi langkah oleh mesin habis'));
124
+ isMesinSibuk = false;
125
+ resolverRequestSekarang = null;
126
+ prosesAntrian();
127
  }
128
  }, 15000);
129
 
130
+ request.resolve = (value) => { clearTimeout(batasWaktuLangkah); request.originalResolve(value); };
131
+ request.reject = (reason) => { clearTimeout(batasWaktuLangkah); request.originalReject(reason); };
 
 
 
 
 
 
 
132
 
133
+ mesin.stdin.write(`position fen ${request.fen}\n`);
134
+ // Waktu mikir mesin per langkah (dalam milidetik)
135
+ mesin.stdin.write('go movetime 1000\n');
136
  }
137
 
138
+ function dapatkanAnalisisDariMesin(fen) {
139
  return new Promise((resolve, reject) => {
140
+ antrianRequest.push({ fen, originalResolve: resolve, originalReject: reject });
141
+ prosesAntrian();
 
 
 
 
142
  });
143
  }
144
 
 
146
  res.setHeader('Access-Control-Allow-Origin', '*');
147
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
148
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
149
+ if (req.method === 'OPTIONS') { return res.sendStatus(204); }
 
 
150
  next();
151
  });
152
 
153
  app.get('/', (req, res) => {
154
  const baseUrl = `${req.protocol}://${req.get('host')}`;
155
+
156
  const doc = `
157
+ DOKUMENTASI API CATUR STOCKFISH
158
+ ===============================
159
 
160
+ API ini menyediakan akses ke mesin catur Stockfish untuk mendapatkan langkah terbaik dan menganalisis permainan lengkap dengan klasifikasi langkah.
161
 
162
  ---------------------------------
163
  ENDPOINT: GET /chessbot
164
  ---------------------------------
165
+ Memberikan analisis lengkap untuk satu posisi FEN, termasuk langkah terbaik dan evaluasi skor dari mesin.
166
 
167
+ METODE: GET
168
 
169
  URL: ${baseUrl}/chessbot
170
 
171
+ PARAMETER QUERY:
172
+ - fen (string, wajib): String FEN dari posisi catur.
173
 
174
+ CONTOH REQUEST:
175
  GET ${baseUrl}/chessbot?fen=rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2
176
 
177
+ CONTOH RESPON (SUKSES):
178
  {
179
  "bestMove": "g1f3",
180
  "from": "g1",
181
  "to": "f3",
182
+ "promotion": null,
183
+ "score": 35
184
  }
185
+ // Catatan: Skor dalam satuan centipawn dari sudut pandang Putih.
186
+ // Nilai positif menguntungkan Putih, nilai negatif menguntungkan Hitam.
187
 
188
+ CONTOH RESPON (ERROR):
189
  {
190
+ "error": "FEN tidak ditemukan"
191
  }
192
 
193
  ---------------------------------
194
  ENDPOINT: POST /analisis
195
  ---------------------------------
196
+ Menganalisis permainan lengkap dari string PGN. Endpoint ini memberikan **respons streaming** (langsung dikirim per bagian) menggunakan Server-Sent Events (SSE). Analisis setiap langkah dikirim sebagai potongan data terpisah segera setelah siap.
197
 
198
+ METODE: POST
199
 
200
  URL: ${baseUrl}/analisis
201
 
202
+ BODY REQUEST:
203
  - Content-Type: application/json
204
+ - Body: { "pgn": "<String-PGN-Anda>" }
205
 
206
+ CONTOH PAYLOAD:
207
  {
208
+ "pgn": "[Event \\"Live Chess\\"]\\n1. e4 Nc6 2. Nf3 f6 3. e5"
209
  }
210
 
211
+ --- FORMAT RESPON STREAMING ---
212
+ Server akan mengirim serangkaian event. Klien Anda harus mendengarkan event 'data'.
213
+
214
+ CONTOH RESPON STREAMING (SUKSES):
215
+ // Setiap baris 'data' adalah objek JSON terpisah yang dikirim server satu per satu.
216
+
217
+ data: {"moveNumber":"1.","playedMove":"e4","bestMove":"e4","fenBeforeMove":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1","classification":"Terbaik","centipawnLoss":0}
218
+
219
+ data: {"moveNumber":"1...","playedMove":"Nc6","bestMove":"e5","fenBeforeMove":"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1","classification":"Kurang Akurat","centipawnLoss":78}
220
+
221
+ data: {"moveNumber":"2.","playedMove":"Nf3","bestMove":"Nf3","fenBeforeMove":"r1bqkbnr/pppppppp/2n5/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 1 2","classification":"Luar Biasa","centipawnLoss":25}
222
+
223
+ data: {"moveNumber":"2...","playedMove":"f6","bestMove":"d5","fenBeforeMove":"r1bqkbnr/pppppppp/2n5/8/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 0 2","classification":"Blunder","centipawnLoss":412}
224
+
225
+ data: {"moveNumber":"3.","playedMove":"e5","bestMove":"d4","fenBeforeMove":"r1bqkbnr/pppp2pp/2n2p2/4P3/8/5N2/PPPP1PPP/RNBQKB1R w KQkq - 0 3","classification":"Kesalahan","centipawnLoss":280}
226
+
227
+ // Setelah langkah terakhir dianalisis, server akan mengirim event 'end'.
228
+ event: end
229
+ data: {"message":"Analisis selesai"}
230
+
231
+ // Catatan: Jika terjadi error di tengah jalan, event 'error' akan dikirim beserta detailnya.
232
+
233
+ CONTOH RESPON (ERROR VALIDASI AWAL):
234
  {
235
+ "error": "Format PGN tidak valid",
236
+ "details": "Invalid PGN"
237
  }
238
  `;
239
  res.setHeader('Content-Type', 'text/plain');
 
242
 
243
  app.get('/chessbot', async (req, res) => {
244
  const fen = req.query.fen;
245
+ if (!fen) { return res.status(400).json({ error: 'FEN tidak ditemukan' }); }
 
 
 
246
  try {
247
+ const analisis = await dapatkanAnalisisDariMesin(fen);
248
+ res.json(analisis);
249
  } catch (e) {
250
  res.status(500).json({ error: e.message });
251
  }
 
253
 
254
  app.post('/analisis', async (req, res) => {
255
  const { pgn } = req.body;
256
+ if (!pgn) { return res.status(400).json({ error: 'PGN tidak ditemukan di body request' }); }
 
 
257
 
258
+ const catur = new Chess();
259
  try {
260
+ catur.loadPgn(pgn.replace(/\\n/g, '\n'));
 
 
 
 
261
  } catch (e) {
262
+ return res.status(400).json({ error: 'Format PGN tidak valid', details: e.message });
263
  }
264
 
265
+ const riwayat = catur.history({ verbose: true });
266
+ if (riwayat.length === 0) { return res.status(400).json({ error: 'PGN tidak berisi langkah yang valid' }); }
267
+
268
+ res.setHeader('Content-Type', 'text/event-stream');
269
+ res.setHeader('Cache-Control', 'no-cache');
270
+ res.setHeader('Connection', 'keep-alive');
271
+ res.flushHeaders();
272
 
273
+ const game = new Chess();
274
  try {
275
+ for (const langkah of riwayat) {
276
+ const fenSebelum = game.fen();
277
+ const giliran = game.turn();
 
278
 
279
+ const analisisSebelum = await dapatkanAnalisisDariMesin(fenSebelum);
280
+ let skorSebelum = analisisSebelum.score;
281
+ if (giliran === 'b') { skorSebelum = -skorSebelum; }
282
+
283
+ game.move(langkah.san);
284
+ const fenSesudah = game.fen();
285
+
286
+ const analisisSesudah = await dapatkanAnalisisDariMesin(fenSesudah);
287
+ let skorSesudah = analisisSesudah.score;
288
+ if (giliran === 'b') { skorSesudah = -skorSesudah; }
 
 
 
 
289
 
290
+ const rugiCentipawn = Math.round(skorSebelum - skorSesudah);
291
+ const klasifikasi = klasifikasiLangkah(rugiCentipawn);
292
+
293
+ let sanLangkahTerbaik = analisisSebelum.bestMove;
294
+ try {
295
+ const gameSementara = new Chess(fenSebelum);
296
+ const hasilLangkah = gameSementara.move({ from: analisisSebelum.from, to: analisisSebelum.to, promotion: analisisSebelum.promotion });
297
+ if (hasilLangkah) { sanLangkahTerbaik = hasilLangkah.san; }
298
+ } catch (e) { /* Abaikan error konversi notasi */ }
299
+
300
+ const hasil = {
301
+ moveNumber: `${game.moveNumber() - (giliran === 'b' ? 0 : 1)}${giliran === 'w' ? '.' : '...'}`,
302
+ playedMove: langkah.san,
303
+ bestMove: sanLangkahTerbaik,
304
+ fenBeforeMove: fenSebelum,
305
+ classification: klasifikasi,
306
+ centipawnLoss: rugiCentipawn < 0 ? 0 : rugiCentipawn,
307
+ };
308
+
309
+ res.write(`data: ${JSON.stringify(hasil)}\n\n`);
310
  }
311
+
312
+ res.write('event: end\ndata: {"message":"Analisis selesai"}\n\n');
313
+ res.end();
314
+
315
  } catch (e) {
316
+ const payloadError = { error: 'Analisis gagal', details: e.message };
317
+ res.write(`event: error\ndata: ${JSON.stringify(payloadError)}\n\n`);
318
+ res.end();
319
  }
320
  });
321
 
322
+ async function jalankanServer() {
323
  try {
324
+ console.log('Lagi nyiapin mesin Stockfish...');
325
+ await siapkanMesin();
326
  app.listen(PORT, () => {
327
+ console.log(`Server jalan di port ${PORT}`);
328
+ console.log(`Dokumentasi API bisa dibuka di http://localhost:${PORT}/`);
329
  });
330
  } catch (error) {
331
+ console.error('Gagal memulai aplikasi:', error.message);
332
  process.exit(1);
333
  }
334
  }
335
 
336
+ jalankanServer();