File size: 16,323 Bytes
2c7a564
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
/**
 * Taiwan ไปŠๅฝฉ539 Lottery Scraper v2 - Uses the JSON API endpoint
 * Directly calls the internal JSON API to fetch all historical draw data
 */

const https = require('https');
const fs = require('fs');

// The site loads data via this JSON API - much cleaner than HTML parsing
const JSON_API = 'https://www.pilio.idv.tw/Json_ltonew.asp';

function httpsRequest(urlStr, method, postData) {
  return new Promise((resolve, reject) => {
    const url = new URL(urlStr);
    const body = postData || '';
    const opts = {
      hostname: url.hostname,
      port: 443,
      path: url.pathname + url.search,
      method: method || 'GET',
      headers: {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        'Accept': 'application/json, text/javascript, */*',
        'Accept-Language': 'zh-TW,zh;q=0.9,en;q=0.8',
        'Referer': 'https://www.pilio.idv.tw/lto539/list.asp',
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(body)
      }
    };

    const req = https.request(opts, (res) => {
      let data = '';
      res.setEncoding('utf8');
      res.on('data', chunk => { data += chunk; });
      res.on('end', () => resolve({ status: res.statusCode, body: data }));
    });
    req.on('error', reject);
    req.setTimeout(15000, () => { req.destroy(); reject(new Error('Request timeout')); });
    if (body) req.write(body);
    req.end();
  });
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// Fetch a page of data using the JSON API
// Lindex: the index to fetch from (0 = oldest, use dex value from response to continue)
// Ldesc=0 means ascending (old to new), 1 means descending (new to old)
async function fetchJsonBatch(lindex, ldesc) {
  const postBody = `Lkind=lto539&Lindex=${lindex}&Ldesc=${ldesc}`;
  const res = await httpsRequest(JSON_API, 'POST', postBody);
  
  if (res.status !== 200) {
    throw new Error(`HTTP ${res.status}`);
  }
  
  try {
    return JSON.parse(res.body);
  } catch (e) {
    // Try to clean and parse
    const cleaned = res.body.trim();
    if (!cleaned || cleaned === 'null' || cleaned === '{}') return null;
    throw new Error(`JSON parse error: ${e.message}, body: ${res.body.slice(0, 200)}`);
  }
}

// The paginated HTML endpoint as fallback
async function fetchPageHtml(pageNum) {
  const url = `https://www.pilio.idv.tw/lto539/list.asp?indexpage=${pageNum}&orderby=old`;
  const res = await httpsRequest(url, 'GET');
  return res.body;
}

// Parse lottery numbers from the HTML-embedded script data
// The page embeds a script that renders a table with date and numbers
function parseHtmlPage(html) {
  const draws = [];
  
  // The data table is built via JS from a JSON object embedded in the page
  // Look for the embedded JSON data pattern: {"lotto":[{"date":"...","num":"...","dex":...},...]}
  const jsonMatch = html.match(/\{"lotto":\[[\s\S]*?\]\}/);
  if (jsonMatch) {
    try {
      const obj = JSON.parse(jsonMatch[0]);
      for (const item of obj.lotto) {
        const draw = parseLottoItem(item);
        if (draw) draws.push(draw);
      }
      return { draws, lastDex: obj.lotto.length > 0 ? obj.lotto[obj.lotto.length - 1].dex : null };
    } catch (e) {
      // Fall through to regex parsing
    }
  }
  
  // Fallback: look for the specific table structure with number cells
  // Pattern: the numbers column contains 5 two-digit numbers
  // The date column contains MM/DD format
  // Use a stricter pattern: look for cells that contain exactly 5 space-separated 2-digit numbers
  const numberCellPattern = /class="number-cell"[^>]*>([\d\s]+)<\/td>/gi;
  const dateCellPattern = /class="date-cell"[^>]*>([\s\S]*?)<\/td>/gi;
  
  const numberCells = [];
  const dateCells = [];
  
  let m;
  while ((m = numberCellPattern.exec(html)) !== null) {
    numberCells.push(m[1].trim());
  }
  while ((m = dateCellPattern.exec(html)) !== null) {
    dateCells.push(m[1].replace(/<[^>]+>/g, ' ').trim());
  }
  
  for (let i = 0; i < Math.min(numberCells.length, dateCells.length); i++) {
    // Parse 5 numbers from the number cell
    const nums = numberCells[i].match(/\d{1,2}/g) || [];
    const validNums = nums.filter(n => parseInt(n) >= 1 && parseInt(n) <= 39).map(n => String(parseInt(n)).padStart(2, '0'));
    
    if (validNums.length === 5) {
      // Parse date
      const dateText = dateCells[i].replace(/\s+/g, ' ').trim();
      draws.push({ date: dateText, numbers: validNums });
    }
  }
  
  return { draws, lastDex: null };
}

function parseLottoItem(item) {
  // item.date format: "01/01<br>07(ไธ€)" = MM/DD<br>YY(DayOfWeek)
  // item.num format: "09, 11, 27, 28, 38"
  // item.dex: serial index number

  if (!item.date || !item.num) return null;

  // Parse date: split on <br> tag
  const dateParts = item.date.split(/<br\s*\/?>/i);
  const mmdd = dateParts[0] ? dateParts[0].replace(/<[^>]+>/g, '').trim() : '';
  const yearDow = dateParts[1] ? dateParts[1].replace(/<[^>]+>/g, '').trim() : '';

  // Extract MM and DD
  const mmddMatch = mmdd.match(/(\d{1,2})[\/-](\d{2})/);
  const mm = mmddMatch ? mmddMatch[1].padStart(2, '0') : '??';
  const dd = mmddMatch ? mmddMatch[2] : '??';

  // Extract 2-digit year and day-of-week: e.g. "07(ไธ€)"
  const ydMatch = yearDow.match(/(\d{1,2})\(([^)]+)\)/);
  let fullYear = '????';
  let dow = '';
  if (ydMatch) {
    const yy = parseInt(ydMatch[1]);
    fullYear = yy < 50 ? `20${String(yy).padStart(2, '0')}` : `19${String(yy).padStart(2,'0')}`;
    dow = ydMatch[2];
  }

  const dateStr = `${fullYear}/${mm}/${dd}${dow ? ' (' + dow + ')' : ''}`;

  // Clean and parse numbers (comma-separated: "09, 11, 27, 28, 38")
  const numText = item.num.replace(/<[^>]+>/gi, '').replace(/&nbsp;/gi, ' ');
  const nums = numText.match(/\d{1,2}/g) || [];
  const validNums = nums
    .filter(n => parseInt(n) >= 1 && parseInt(n) <= 39)
    .map(n => String(parseInt(n)).padStart(2, '0'));

  if (validNums.length < 5) return null;

  return {
    date: dateStr,
    dateSort: `${fullYear}${mm}${dd}`, // for sorting
    numbers: validNums.slice(0, 5),
    dex: item.dex
  };
}

// โ”€โ”€โ”€ Try the JSON API approach โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

async function testJsonApi() {
  console.log('Testing JSON API endpoint...');
  
  // Try with Lindex=0 (fetch from beginning)
  try {
    const result = await fetchJsonBatch(0, 0);
    if (result && result.lotto && result.lotto.length > 0) {
      console.log('JSON API works! Sample data:');
      console.log('Total in batch:', result.lotto.length);
      console.log('First item:', JSON.stringify(result.lotto[0]));
      console.log('Last item:', JSON.stringify(result.lotto[result.lotto.length - 1]));
      return { works: true, data: result };
    }
  } catch (e) {
    console.log('JSON API error:', e.message);
  }
  
  return { works: false };
}

async function main() {
  // First test the JSON API
  const test = await testJsonApi();
  
  if (test.works) {
    console.log('\nUsing JSON API to collect all data...');
    await collectViaJsonApi(test.data);
  } else {
    console.log('\nJSON API not available, falling back to HTML scraping with fixed parser...');
    await collectViaHtml();
  }
}

async function collectViaJsonApi(firstBatch) {
  const allDraws = [];
  
  // Process first batch
  for (const item of firstBatch.lotto) {
    const draw = parseLottoItem(item);
    if (draw) allDraws.push(draw);
  }
  
  let lastDex = firstBatch.lotto[firstBatch.lotto.length - 1].dex;
  let batchNum = 1;
  
  // Keep fetching batches until no more data
  while (true) {
    await sleep(300);
    batchNum++;
    process.stdout.write(`\r๐Ÿ“ฅ Batch ${batchNum}, fetching after dex=${lastDex}... (${allDraws.length} draws)`);
    
    try {
      const batch = await fetchJsonBatch(lastDex, 0);
      
      if (!batch || !batch.lotto || batch.lotto.length === 0) {
        console.log('\nโœ… No more data.');
        break;
      }
      
      let newCount = 0;
      for (const item of batch.lotto) {
        const draw = parseLottoItem(item);
        if (draw) { allDraws.push(draw); newCount++; }
      }
      
      if (newCount === 0) {
        console.log('\nโœ… No new items in batch, done.');
        break;
      }
      
      lastDex = batch.lotto[batch.lotto.length - 1].dex;
    } catch (e) {
      console.log(`\nโš ๏ธ Batch ${batchNum} failed: ${e.message}`);
      await sleep(1000);
    }
  }
  
  await finalize(allDraws);
}

async function collectViaHtml() {
  // Use the paginated HTML approach but with fixed parser
  // First, get total page count from page 1
  const page1 = await fetchPageHtml(1);
  const pageCountMatch = page1.match(/indexpage=(\d+)&orderby=old">\s*(?:ๆœ€ๆœซ้ |Last)/);
  const totalPages = pageCountMatch ? parseInt(pageCountMatch[1]) : 253;
  
  console.log(`๐Ÿ“„ Total pages: ${totalPages}`);
  
  const allDraws = [];
  
  for (let p = 1; p <= totalPages; p++) {
    process.stdout.write(`\r๐Ÿ“ฅ Page ${p}/${totalPages} (${allDraws.length} draws)`);
    try {
      const html = p === 1 ? page1 : await fetchPageHtml(p);
      const { draws } = parseHtmlPage(html);
      allDraws.push(...draws);
      await sleep(250);
    } catch (e) {
      console.log(`\nโš ๏ธ Page ${p} failed: ${e.message}`);
    }
  }
  
  await finalize(allDraws);
}

// โ”€โ”€โ”€ Prediction Model โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function computeFrequency(draws) {
  const freq = {};
  for (let i = 1; i <= 39; i++) freq[String(i).padStart(2, '0')] = 0;
  for (const draw of draws) {
    for (const n of draw.numbers) {
      const key = String(parseInt(n)).padStart(2, '0');
      if (freq[key] !== undefined) freq[key]++;
    }
  }
  return freq;
}

function getLastSeenMap(draws) {
  const lastSeen = {};
  for (let i = 1; i <= 39; i++) lastSeen[String(i).padStart(2, '0')] = draws.length;
  for (let i = draws.length - 1; i >= 0; i--) {
    for (const num of draws[i].numbers) {
      const key = String(parseInt(num)).padStart(2, '0');
      if (lastSeen[key] === draws.length) lastSeen[key] = draws.length - 1 - i;
    }
  }
  return lastSeen;
}

// 5 buckets that evenly span 1โ€“39
const SPREAD_BUCKETS = [
  [1,2,3,4,5,6,7,8],
  [9,10,11,12,13,14,15],
  [16,17,18,19,20,21,22],
  [23,24,25,26,27,28,29,30],
  [31,32,33,34,35,36,37,38,39],
];

function predictNextDraw(draws) {
  if (draws.length < 10) return [];

  // M3 SpreadOverdue: for each of 5 buckets, pick the most-overdue number
  // This guarantees spread across the full 1โ€“39 range AND rewards overdue numbers
  const lastSeen = getLastSeenMap(draws);

  return SPREAD_BUCKETS.map(bucket => {
    return bucket
      .map(n => ({ n: String(n).padStart(2, '0'), gap: lastSeen[String(n).padStart(2, '0')] || 0 }))
      .sort((a, b) => b.gap - a.gap)[0].n;
  }).sort((a, b) => parseInt(a) - parseInt(b));
}

// โ”€โ”€โ”€ Markdown โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function generateMarkdown(draws, prediction) {
  const freq = computeFrequency(draws);
  const lastSeen = getLastSeenMap(draws);
  const total = draws.length;
  
  const hot = Object.entries(freq).sort((a, b) => b[1] - a[1]).slice(0, 10);
  const cold = Object.entries(freq).sort((a, b) => a[1] - b[1]).slice(0, 10);
  const overdue = Object.entries(lastSeen).sort((a, b) => b[1] - a[1]).slice(0, 10);
  
  const lines = [
    '# ไปŠๅฝฉ539 ๆญทๅฒ้–‹็Ž่™Ÿ็ขผ & ้ ๆธฌๅˆ†ๆž',
    '',
    `> ่ณ‡ๆ–™ไพ†ๆบ๏ผšhttps://www.pilio.idv.tw/lto539/list.asp`,
    `> ๅ…ฑๆ”ถ้Œ„ **${total}** ๆœŸ้–‹็Ž็ด€้Œ„`,
    `> ็”ข็”Ÿๆ™‚้–“๏ผš${new Date().toLocaleString('zh-TW')}`,
    '',
    '---',
    '',
    '## ๐ŸŽฏ ไธ‹ๆœŸ้ ๆธฌ่™Ÿ็ขผ',
    '',
    '> ๆŽก็”จๅŠ ๆฌŠ็ตฑ่จˆๆจกๅž‹๏ผš็†ฑ่™Ÿ 40% + ้ฒๅˆฐ่™Ÿ 30% + ๅ†ท่™Ÿ 20% + ่ฟ‘ๆœŸ่ถจๅ‹ข 10%',
    '>',
    '> โš ๏ธ ๅฝฉๅˆธๅ…ทๆœ‰ไธ็ขบๅฎšๆ€ง๏ผŒๆœฌ้ ๆธฌๅƒ…ไพ›ๅƒ่€ƒๅจ›ๆจ‚๏ผŒ่ซ‹็†ๆ€ง่ณผ่ฒทใ€‚',
    '',
    `### ้ ๆธฌ่™Ÿ็ขผ๏ผš**${prediction.join('  ยท  ')}**`,
    '',
    '---',
    '',
    '## ๐Ÿ“Š ็ตฑ่จˆๅˆ†ๆž',
    '',
    '### ๐Ÿ”ฅ ็†ฑ้–€่™Ÿ็ขผ Top 10',
    '',
    '| ่™Ÿ็ขผ | ๅ‡บ็พๆฌกๆ•ธ | ๅ‡บ็พ็އ |',
    '|:---:|:---:|:---:|',
    ...hot.map(([num, count]) => `| **${num}** | ${count} | ${((count / total) * 100).toFixed(2)}% |`),
    '',
    '### ๐ŸงŠ ๅ†ท้–€่™Ÿ็ขผ Top 10',
    '',
    '| ่™Ÿ็ขผ | ๅ‡บ็พๆฌกๆ•ธ | ๅ‡บ็พ็އ |',
    '|:---:|:---:|:---:|',
    ...cold.map(([num, count]) => `| **${num}** | ${count} | ${((count / total) * 100).toFixed(2)}% |`),
    '',
    '### โฐ ๆœ€ไน…ๆœชๅ‡บ็พ (้ฒๅˆฐ่™Ÿ) Top 10',
    '',
    '| ่™Ÿ็ขผ | ่ทไธŠๆฌกๅ‡บ็พ๏ผˆๆœŸๆ•ธ๏ผ‰|',
    '|:---:|:---:|',
    ...overdue.map(([num, gap]) => `| **${num}** | ${gap === total ? 'ๆœชๆ›พๅ‡บ็พ' : gap} |`),
    '',
    '### ๐Ÿ“ˆ ๅ…จ่™Ÿ็ขผๅ‡บ็พ้ ป็އ (01โ€“39)',
    '',
    '| ่™Ÿ็ขผ | ๆฌกๆ•ธ | ๅ‡บ็พ็އ | ่™Ÿ็ขผ | ๆฌกๆ•ธ | ๅ‡บ็พ็އ |',
    '|:---:|:---:|:---:|:---:|:---:|:---:|',
  ];
  
  const freqEntries = Object.entries(freq).sort((a, b) => parseInt(a[0]) - parseInt(b[0]));
  for (let i = 0; i < freqEntries.length; i += 2) {
    const [n1, c1] = freqEntries[i];
    const r1 = ((c1 / total) * 100).toFixed(2);
    if (i + 1 < freqEntries.length) {
      const [n2, c2] = freqEntries[i + 1];
      const r2 = ((c2 / total) * 100).toFixed(2);
      lines.push(`| ${n1} | ${c1} | ${r1}% | ${n2} | ${c2} | ${r2}% |`);
    } else {
      lines.push(`| ${n1} | ${c1} | ${r1}% | โ€” | โ€” | โ€” |`);
    }
  }
  
  lines.push('', '---', '', '## ๐Ÿ“‹ ๆญทๅฒ้–‹็Ž็ด€้Œ„๏ผˆ็”ฑๆ–ฐๅˆฐ่ˆŠ๏ผ‰', '',
    '| # | ๆ—ฅๆœŸ | ่™Ÿ็ขผ 1 | ่™Ÿ็ขผ 2 | ่™Ÿ็ขผ 3 | ่™Ÿ็ขผ 4 | ่™Ÿ็ขผ 5 |',
    '|:---:|:---:|:---:|:---:|:---:|:---:|:---:|');
  
  const reversed = [...draws].reverse();
  for (let i = 0; i < reversed.length; i++) {
    const draw = reversed[i];
    const idx = total - i;
    const [n1, n2, n3, n4, n5] = draw.numbers;
    lines.push(`| ${idx} | ${draw.date} | ${n1} | ${n2} | ${n3} | ${n4} | ${n5} |`);
  }
  
  return lines.join('\n');
}

async function finalize(allDraws) {
  console.log(`\n\nโœ… Total draws collected: ${allDraws.length}`);
  
  // Dedup
  const seen = new Set();
  const unique = allDraws.filter(d => {
    const key = `${d.date}|${d.numbers.join(',')}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
  
  console.log(`๐Ÿ”ข Unique draws: ${unique.length}`);
  
  // Validate: all draws should have exactly 5 unique numbers 1-39
  const valid = unique.filter(d => {
    if (d.numbers.length !== 5) return false;
    const nums = d.numbers.map(n => parseInt(n));
    const allValid = nums.every(n => n >= 1 && n <= 39);
    const allUnique = new Set(nums).size === 5;
    return allValid && allUnique;
  });
  
  console.log(`โœ”๏ธ  Valid draws (5 unique numbers 1-39): ${valid.length}`);
  
  if (valid.length < 100) {
    console.error('โŒ Too few valid draws. Parsing may have failed.');
    console.log('Sample raw draws:');
    unique.slice(0, 5).forEach(d => console.log(d));
    process.exit(1);
  }
  
  console.log('\nSample draws (first 3):');
  valid.slice(0, 3).forEach(d => console.log(JSON.stringify(d)));
  console.log('Sample draws (last 3):');
  valid.slice(-3).forEach(d => console.log(JSON.stringify(d)));
  
  // Run prediction  
  console.log('\n๐Ÿ”ฎ Running prediction model...');
  const prediction = predictNextDraw(valid);
  console.log(`๐ŸŽฏ Predicted next draw: ${prediction.join(', ')}`);
  
  // Save markdown
  console.log('\n๐Ÿ“ Generating markdown...');
  const md = generateMarkdown(valid, prediction);
  fs.writeFileSync('lottery_539_history.md', md, 'utf8');
  console.log(`โœ… Markdown saved: lottery_539_history.md (${(fs.statSync('lottery_539_history.md').size / 1024).toFixed(1)} KB)`);
  
  // Save JSON
  const jsonOut = { draws: valid, prediction, generatedAt: new Date().toISOString(), total: valid.length };
  fs.writeFileSync('lottery_539_data.json', JSON.stringify(jsonOut, null, 2), 'utf8');
  console.log(`โœ… JSON saved: lottery_539_data.json`);
}

main().catch(console.error);