silent-akinator-pro 2.0.1 → 3.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/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) Silent Tech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
package/README.md CHANGED
@@ -1,8 +1,23 @@
1
1
  # silent-akinator-pro
2
2
 
3
- > **Silent Tech** — a modern, working Akinator API wrapper for Node.js.
3
+ > 🧞 **The working Akinator API wrapper for Node.js** — fast, reliable, Cloudflare-safe. Modern Akinator client by **Silent Tech**.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/silent-akinator-pro.svg)](https://www.npmjs.com/package/silent-akinator-pro)
6
+ [![downloads](https://img.shields.io/npm/dm/silent-akinator-pro.svg)](https://www.npmjs.com/package/silent-akinator-pro)
7
+ [![license](https://img.shields.io/npm/l/silent-akinator-pro.svg)](LICENSE)
8
+
9
+ The most up-to-date **Akinator** Node.js wrapper that actually works in 2026. Bypasses the Cloudflare `403` by using the official HTML form endpoints with realistic browser headers and a full cookie jar — no third-party proxies, no API keys.
10
+
11
+ ## Why silent-akinator-pro?
12
+
13
+ - ✅ **Working** — fixed the `403 Forbidden` / Cloudflare issue that broke every other akinator npm package
14
+ - ⚡ **Fast** — zero heavy dependencies (only `tough-cookie`)
15
+ - 🌍 **16 regions** — `en`, `fr`, `es`, `de`, `it`, `pt`, `ru`, `ar`, `jp`, `kr`, `cn`, `nl`, `pl`, `tr`, `il`, `id`
16
+ - 🧒 **Child mode** supported
17
+ - â†Šī¸ **Undo / back / continue / win** all supported
18
+ - 🤖 **Discord bot / Telegram bot / WhatsApp bot ready**
19
+ - 📘 **TypeScript types** included
20
+ - 🆓 **MIT** licensed
6
21
 
7
22
  ## Install
8
23
 
@@ -20,12 +35,16 @@ const { Akinator } = require('silent-akinator-pro');
20
35
  await aki.start();
21
36
 
22
37
  console.log(aki.question);
23
- await aki.answer('y'); // y / n / idk / p / pn
38
+ await aki.answer('y'); // y / n / idk / p / pn (or 0-4)
24
39
  console.log(aki.question);
25
40
 
26
- // ...keep answering until aki.guessed === true
27
- if (aki.guess) {
41
+ while (!aki.guessed && aki.step < 80) {
42
+ await aki.answer('idk');
43
+ }
44
+
45
+ if (aki.guessed) {
28
46
  console.log('Akinator thinks of:', aki.guess.name);
47
+ await aki.win();
29
48
  }
30
49
  })();
31
50
  ```
@@ -33,20 +52,46 @@ const { Akinator } = require('silent-akinator-pro');
33
52
  ## API
34
53
 
35
54
  ### `new Akinator({ region, childMode })`
36
- - `region` — `en`, `ar`, `cn`, `de`, `es`, `fr`, `il`, `it`, `jp`, `kr`, `nl`, `pl`, `pt`, `ru`, `tr`, `id`
37
- - `childMode` — boolean, filters adult content
55
+ - `region` — `en | fr | es | de | it | pt | ru | ar | jp | kr | cn | nl | pl | tr | il | id`
56
+ - `childMode` — `boolean`
38
57
 
39
58
  ### Methods
40
- | Method | Description |
41
- |--------|-------------|
42
- | `start()` | Begin a new game |
43
- | `answer(a)` | `y` / `n` / `idk` / `p` / `pn` (or 0-4) |
44
- | `back()` | Undo the previous answer |
45
- | `continue()` | Reject the current guess and keep playing |
46
- | `win()` | Confirm the guess and end the game |
59
+ | Method | Description |
60
+ |---------------|----------------------------------------------|
61
+ | `start()` | Begin a new game |
62
+ | `answer(a)` | `y` / `n` / `idk` / `p` / `pn` (or `0-4`) |
63
+ | `back()` | Undo the previous answer |
64
+ | `continue()` | Reject the current guess and keep playing |
65
+ | `win()` | Confirm the guess and end the game |
47
66
 
48
67
  ### Properties
49
68
  `question`, `step`, `progression`, `guess`, `guessed`, `finished`.
50
69
 
70
+ ## Discord bot example
71
+
72
+ ```js
73
+ const { Akinator } = require('silent-akinator-pro');
74
+ const games = new Map();
75
+
76
+ client.on('messageCreate', async (msg) => {
77
+ if (msg.content === '!aki') {
78
+ const aki = new Akinator({ region: 'en' });
79
+ await aki.start();
80
+ games.set(msg.author.id, aki);
81
+ msg.reply(aki.question);
82
+ } else if (['y','n','idk','p','pn'].includes(msg.content)) {
83
+ const aki = games.get(msg.author.id);
84
+ if (!aki) return;
85
+ await aki.answer(msg.content);
86
+ if (aki.guessed) msg.reply(`Is it **${aki.guess.name}**? (${aki.guess.description})`);
87
+ else msg.reply(aki.question);
88
+ }
89
+ });
90
+ ```
91
+
92
+ ## Keywords
93
+ akinator, akinator api, akinator nodejs, akinator wrapper, akinator client, akinator bot, akinator discord, akinator 2026, working akinator, akinator cloudflare, silent tech, silent-akinator, silent-akinator-pro, twenty questions, character guessing game, genie api.
94
+
51
95
  ## License
52
- MIT Š Silent Tech
96
+
97
+ MIT Š **Silent Tech**
package/index.d.ts ADDED
@@ -0,0 +1,26 @@
1
+
2
+ export interface AkinatorOptions {
3
+ region?: 'en'|'ar'|'cn'|'de'|'es'|'fr'|'il'|'it'|'jp'|'kr'|'nl'|'pl'|'pt'|'ru'|'tr'|'id';
4
+ childMode?: boolean;
5
+ }
6
+ export interface AkinatorGuess {
7
+ id?: string; name?: string; description?: string; photo?: string; pseudo?: string;
8
+ }
9
+ export class Akinator {
10
+ constructor(opts?: AkinatorOptions);
11
+ region: string;
12
+ childMode: boolean;
13
+ question: string | null;
14
+ step: number;
15
+ progression: string;
16
+ guess: AkinatorGuess | null;
17
+ guessed: boolean;
18
+ finished: boolean;
19
+ start(): Promise<this>;
20
+ answer(a: 'y'|'n'|'idk'|'p'|'pn'|0|1|2|3|4|string|number): Promise<this>;
21
+ back(): Promise<this>;
22
+ continue(): Promise<this>;
23
+ win(): Promise<AkinatorGuess>;
24
+ }
25
+ export const REGIONS: Record<string,string>;
26
+ export default Akinator;
package/index.js ADDED
@@ -0,0 +1,245 @@
1
+
2
+ 'use strict';
3
+ /*!
4
+ * silent-akinator-pro
5
+ * Working Akinator API wrapper (HTML-form endpoints, Cloudflare-safe).
6
+ * (c) Silent Tech — MIT
7
+ */
8
+ const { CookieJar } = require('tough-cookie');
9
+ const https = require('https');
10
+ const { URL, URLSearchParams } = require('url');
11
+
12
+ const REGIONS = {
13
+ en: 'en', ar: 'ar', cn: 'cn', de: 'de', es: 'es', fr: 'fr',
14
+ il: 'il', it: 'it', jp: 'jp', kr: 'kr', nl: 'nl', pl: 'pl',
15
+ pt: 'pt', ru: 'ru', tr: 'tr', id: 'id'
16
+ };
17
+
18
+ const ANSWER_MAP = {
19
+ y: 0, yes: 0, '0': 0,
20
+ n: 1, no: 1, '1': 1,
21
+ idk: 2, "don't know": 2, dk: 2, '2': 2,
22
+ p: 3, probably: 3, '3': 3,
23
+ pn: 4, 'probably not': 4, '4': 4
24
+ };
25
+
26
+ const UA =
27
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
28
+ '(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
29
+
30
+ function request(method, urlStr, { headers = {}, body = null, jar } = {}) {
31
+ return new Promise((resolve, reject) => {
32
+ const u = new URL(urlStr);
33
+ const cookieHeader = jar ? jar.getCookieStringSync(urlStr) : '';
34
+ const opts = {
35
+ method,
36
+ hostname: u.hostname,
37
+ path: u.pathname + u.search,
38
+ headers: {
39
+ 'User-Agent': UA,
40
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
41
+ 'Accept-Language': 'en-US,en;q=0.9',
42
+ 'Accept-Encoding': 'identity',
43
+ 'Referer': `https://${u.hostname}/`,
44
+ 'Origin': `https://${u.hostname}`,
45
+ ...(cookieHeader ? { Cookie: cookieHeader } : {}),
46
+ ...headers
47
+ }
48
+ };
49
+ if (body) {
50
+ opts.headers['Content-Type'] = 'application/x-www-form-urlencoded';
51
+ opts.headers['Content-Length'] = Buffer.byteLength(body);
52
+ opts.headers['X-Requested-With'] = 'XMLHttpRequest';
53
+ }
54
+ const req = https.request(opts, (res) => {
55
+ const chunks = [];
56
+ res.on('data', (c) => chunks.push(c));
57
+ res.on('end', () => {
58
+ const data = Buffer.concat(chunks).toString('utf8');
59
+ if (jar && res.headers['set-cookie']) {
60
+ for (const c of res.headers['set-cookie']) {
61
+ try { jar.setCookieSync(c, urlStr); } catch (_) {}
62
+ }
63
+ }
64
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
65
+ const next = new URL(res.headers.location, urlStr).toString();
66
+ return resolve(request('GET', next, { jar, headers }));
67
+ }
68
+ resolve({ status: res.statusCode, headers: res.headers, body: data });
69
+ });
70
+ });
71
+ req.on('error', reject);
72
+ if (body) req.write(body);
73
+ req.end();
74
+ });
75
+ }
76
+
77
+ function extract(html, re) {
78
+ const m = html.match(re);
79
+ return m ? m[1] : null;
80
+ }
81
+
82
+ class Akinator {
83
+ constructor(opts = {}) {
84
+ this.region = REGIONS[(opts.region || 'en').toLowerCase()] || 'en';
85
+ this.childMode = !!opts.childMode;
86
+ this.host = `https://${this.region}.akinator.com`;
87
+ this.jar = new CookieJar();
88
+
89
+ this.session = null;
90
+ this.signature = null;
91
+ this.question = null;
92
+ this.step = 0;
93
+ this.progression = '0.00000';
94
+ this.guess = null;
95
+ this.guessed = false;
96
+ this.finished = false;
97
+ }
98
+
99
+ async start() {
100
+ // 1. warm-up to set Cloudflare cf_clearance / __cf_bm cookies
101
+ await request('GET', this.host + '/', { jar: this.jar });
102
+
103
+ // 2. fetch the game page (the HTML form gives us session + signature)
104
+ const url = `${this.host}/game?cm=${this.childMode ? 'true' : 'false'}`;
105
+ const r = await request('GET', url, { jar: this.jar });
106
+ if (r.status !== 200) {
107
+ throw new Error(`Akinator start failed (${r.status}). Region "${this.region}" may be blocked — try another region.`);
108
+ }
109
+
110
+ this.session = extract(r.body, /#session['"]?\s*value=['"]([^'"]+)/i)
111
+ || extract(r.body, /name=['"]session['"][^>]*value=['"]([^'"]+)/i)
112
+ || extract(r.body, /var\s+session\s*=\s*['"]([^'"]+)/i);
113
+ this.signature = extract(r.body, /#signature['"]?\s*value=['"]([^'"]+)/i)
114
+ || extract(r.body, /name=['"]signature['"][^>]*value=['"]([^'"]+)/i)
115
+ || extract(r.body, /var\s+signature\s*=\s*['"]([^'"]+)/i);
116
+ this.question = extract(r.body, /<p[^>]*class=['"][^'"]*question-text[^'"]*['"][^>]*>([^<]+)<\/p>/i)
117
+ || extract(r.body, /id=['"]question-label['"][^>]*>([^<]+)</i);
118
+ this.step = 0;
119
+ this.progression = '0.00000';
120
+
121
+ if (!this.session || !this.signature) {
122
+ throw new Error('Could not parse session/signature from Akinator page. The site layout may have changed.');
123
+ }
124
+ return this;
125
+ }
126
+
127
+ _answerCode(a) {
128
+ if (typeof a === 'number') a = String(a);
129
+ const code = ANSWER_MAP[String(a).toLowerCase().trim()];
130
+ if (code === undefined) throw new Error(`Invalid answer "${a}". Use y / n / idk / p / pn.`);
131
+ return code;
132
+ }
133
+
134
+ async answer(a) {
135
+ if (this.finished) throw new Error('Game already finished.');
136
+ const ans = this._answerCode(a);
137
+ const body = new URLSearchParams({
138
+ step: String(this.step),
139
+ progression: String(this.progression),
140
+ sid: '1',
141
+ cm: this.childMode ? 'true' : 'false',
142
+ answer: String(ans),
143
+ step_last_proposition: '',
144
+ session: this.session,
145
+ signature: this.signature
146
+ }).toString();
147
+
148
+ const r = await request('POST', this.host + '/answer', { jar: this.jar, body });
149
+ let data;
150
+ try { data = JSON.parse(r.body); }
151
+ catch { throw new Error(`Akinator answer failed (status ${r.status}). Response was not JSON.`); }
152
+
153
+ if (data.completion && data.completion !== 'OK' && data.completion !== 'KO - TIMEOUT') {
154
+ // proposal step (Akinator wants to guess)
155
+ if (data.id_proposition || data.name_proposition) {
156
+ this.guess = {
157
+ id: data.id_proposition,
158
+ name: data.name_proposition,
159
+ description: data.description_proposition,
160
+ photo: data.photo,
161
+ pseudo: data.pseudo
162
+ };
163
+ this.guessed = true;
164
+ return this;
165
+ }
166
+ }
167
+
168
+ if (data.id_proposition || data.name_proposition) {
169
+ this.guess = {
170
+ id: data.id_proposition,
171
+ name: data.name_proposition,
172
+ description: data.description_proposition,
173
+ photo: data.photo,
174
+ pseudo: data.pseudo
175
+ };
176
+ this.guessed = true;
177
+ return this;
178
+ }
179
+
180
+ this.question = data.question || this.question;
181
+ this.step = parseInt(data.step ?? this.step + 1, 10);
182
+ this.progression = data.progression || this.progression;
183
+ this.guessed = false;
184
+ this.guess = null;
185
+ return this;
186
+ }
187
+
188
+ async back() {
189
+ if (this.step <= 0) throw new Error('Cannot go back from step 0.');
190
+ const body = new URLSearchParams({
191
+ step: String(this.step),
192
+ progression: String(this.progression),
193
+ sid: '1',
194
+ cm: this.childMode ? 'true' : 'false',
195
+ session: this.session,
196
+ signature: this.signature
197
+ }).toString();
198
+ const r = await request('POST', this.host + '/cancel_answer', { jar: this.jar, body });
199
+ const data = JSON.parse(r.body);
200
+ this.question = data.question || this.question;
201
+ this.step = parseInt(data.step ?? Math.max(0, this.step - 1), 10);
202
+ this.progression = data.progression || this.progression;
203
+ this.guessed = false;
204
+ this.guess = null;
205
+ return this;
206
+ }
207
+
208
+ async continue() {
209
+ if (!this.guessed) throw new Error('No active guess to reject.');
210
+ const body = new URLSearchParams({
211
+ step: String(this.step),
212
+ progression: '10',
213
+ sid: '1',
214
+ cm: this.childMode ? 'true' : 'false',
215
+ session: this.session,
216
+ signature: this.signature
217
+ }).toString();
218
+ const r = await request('POST', this.host + '/exclude', { jar: this.jar, body });
219
+ const data = JSON.parse(r.body);
220
+ this.question = data.question || this.question;
221
+ this.step = parseInt(data.step ?? this.step + 1, 10);
222
+ this.progression = data.progression || this.progression;
223
+ this.guessed = false;
224
+ this.guess = null;
225
+ return this;
226
+ }
227
+
228
+ async win() {
229
+ if (!this.guessed) throw new Error('No guess to confirm.');
230
+ const body = new URLSearchParams({
231
+ sid: '1',
232
+ charac_name: this.guess.name || '',
233
+ charac_desc: this.guess.description || '',
234
+ session: this.session,
235
+ signature: this.signature,
236
+ pflag_photo: '0'
237
+ }).toString();
238
+ await request('POST', this.host + '/choice', { jar: this.jar, body });
239
+ this.finished = true;
240
+ return this.guess;
241
+ }
242
+ }
243
+
244
+ module.exports = { Akinator, REGIONS };
245
+ module.exports.default = Akinator;
package/package.json CHANGED
@@ -1,34 +1,67 @@
1
1
  {
2
2
  "name": "silent-akinator-pro",
3
- "version": "2.0.1",
4
- "description": "Silent Tech \u2014 A modern, working Akinator API wrapper for Node.js. Play the genie game programmatically.",
5
- "main": "src/index.js",
6
- "types": "src/index.d.ts",
3
+ "version": "3.0.0",
4
+ "description": "Akinator API wrapper for Node.js \u2014 fast, working, Cloudflare-safe. The genie that guesses any character. Modern Akinator client with full region, child-mode, undo, and guess-confirm support. (silent-tech)",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "type": "commonjs",
7
8
  "scripts": {
8
- "test": "node examples/basic.js"
9
+ "test": "node test.js"
9
10
  },
10
11
  "keywords": [
11
12
  "akinator",
13
+ "akinator-api",
14
+ "akinator-wrapper",
15
+ "akinator-client",
16
+ "akinator-node",
17
+ "akinator-js",
18
+ "akinator-bot",
19
+ "akinator-game",
12
20
  "silent",
13
21
  "silent-tech",
14
22
  "silent-akinator",
15
- "game",
23
+ "silent-akinator-pro",
16
24
  "genie",
25
+ "guess",
26
+ "guessing-game",
27
+ "20-questions",
28
+ "twenty-questions",
29
+ "character-guess",
30
+ "ai-guess",
31
+ "game",
17
32
  "api",
18
- "wrapper"
33
+ "wrapper",
34
+ "client",
35
+ "cloudflare-bypass",
36
+ "working-akinator",
37
+ "akinator-2025",
38
+ "akinator-2026",
39
+ "discord-bot",
40
+ "telegram-bot",
41
+ "whatsapp-bot",
42
+ "chatbot",
43
+ "fun-api"
19
44
  ],
20
- "author": "Silent Tech",
45
+ "author": "Silent Tech <silent.tech.offc@gmail.com>",
21
46
  "license": "MIT",
22
- "dependencies": {
23
- "axios": "^1.7.7",
24
- "cheerio": "^1.0.0"
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/silent-tech/silent-akinator-pro.git"
50
+ },
51
+ "homepage": "https://www.npmjs.com/package/silent-akinator-pro",
52
+ "bugs": {
53
+ "url": "https://github.com/silent-tech/silent-akinator-pro/issues"
25
54
  },
26
55
  "engines": {
27
56
  "node": ">=14"
28
57
  },
29
- "repository": {
30
- "type": "git",
31
- "url": "https://github.com/silent-tech/silent-akinator-pro"
58
+ "dependencies": {
59
+ "tough-cookie": "^4.1.4"
32
60
  },
33
- "homepage": "https://www.npmjs.com/package/silent-akinator-pro"
61
+ "files": [
62
+ "index.js",
63
+ "index.d.ts",
64
+ "README.md",
65
+ "LICENSE"
66
+ ]
34
67
  }
package/src/index.d.ts DELETED
@@ -1,33 +0,0 @@
1
-
2
- export interface AkinatorOptions {
3
- region?: 'en'|'ar'|'cn'|'de'|'es'|'fr'|'il'|'it'|'jp'|'kr'|'nl'|'pl'|'pt'|'ru'|'tr'|'id';
4
- childMode?: boolean;
5
- }
6
- export interface Guess {
7
- id: string;
8
- name: string;
9
- description: string;
10
- image: string;
11
- }
12
- export type AnswerInput = 'y'|'n'|'idk'|'p'|'pn'|0|1|2|3|4|string|number;
13
-
14
- export class Akinator {
15
- constructor(options?: AkinatorOptions);
16
- region: string;
17
- childMode: boolean;
18
- session: string | null;
19
- signature: string | null;
20
- step: number;
21
- progression: string;
22
- question: string | null;
23
- guess: Guess | null;
24
- guessed: boolean;
25
- finished: boolean;
26
- start(): Promise<{ question: string; step: number; progression: string }>;
27
- answer(ans: AnswerInput): Promise<{ question?: string; step?: number; progression?: string; guess?: Guess }>;
28
- back(): Promise<{ question: string; step: number; progression: string }>;
29
- continue(): Promise<{ question: string; step: number; progression: string }>;
30
- win(): Promise<{ winner: Guess | null }>;
31
- }
32
- export const REGIONS: string[];
33
- export default Akinator;
package/src/index.js DELETED
@@ -1,244 +0,0 @@
1
-
2
- /**
3
- * silent-akinator-pro
4
- * Silent Tech — Working Akinator API wrapper.
5
- *
6
- * Usage:
7
- * const { Akinator } = require('silent-akinator-pro');
8
- * const aki = new Akinator({ region: 'en', childMode: false });
9
- * await aki.start();
10
- * console.log(aki.question);
11
- * await aki.answer('y'); // y / n / idk / p / pn
12
- * if (aki.guess) console.log(aki.guess);
13
- */
14
-
15
- const axios = require('axios');
16
- const cheerio = require('cheerio');
17
-
18
- const REGIONS = ['en','ar','cn','de','es','fr','il','it','jp','kr','nl','pl','pt','ru','tr','id'];
19
-
20
- const ANSWER_MAP = {
21
- // yes
22
- 'y': 0, 'yes': 0, 'oui': 0, '0': 0,
23
- // no
24
- 'n': 1, 'no': 1, 'non': 1, '1': 1,
25
- // don't know
26
- 'idk': 2, 'dk': 2, "don't know": 2, 'dont know': 2, '2': 2,
27
- // probably
28
- 'p': 3, 'probably': 3, '3': 3,
29
- // probably not
30
- 'pn': 4, 'probably not': 4, '4': 4
31
- };
32
-
33
- class Akinator {
34
- constructor({ region = 'en', childMode = false } = {}) {
35
- if (!REGIONS.includes(region)) {
36
- throw new Error(`Invalid region "${region}". Use one of: ${REGIONS.join(', ')}`);
37
- }
38
- this.region = region;
39
- this.childMode = !!childMode;
40
- this.baseURL = `https://${region}.akinator.com`;
41
- this.session = null;
42
- this.signature = null;
43
- this.step = 0;
44
- this.progression = '0.00000';
45
- this.question = null;
46
- this.guess = null; // { name, description, image, id }
47
- this.guessed = false;
48
- this.finished = false;
49
- this._client = axios.create({
50
- baseURL: this.baseURL,
51
- headers: {
52
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 SilentTech/1.0',
53
- 'Accept-Language': 'en-US,en;q=0.9',
54
- 'Origin': this.baseURL,
55
- 'Referer': `${this.baseURL}/game`,
56
- 'X-Requested-With': 'XMLHttpRequest'
57
- },
58
- timeout: 20000,
59
- validateStatus: () => true
60
- });
61
- }
62
-
63
- /** Begin a new game session. */
64
- async start() {
65
- const body = new URLSearchParams({
66
- sid: '1',
67
- cm: this.childMode ? 'true' : 'false'
68
- }).toString();
69
-
70
- const res = await this._client.post('/game', body, {
71
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
72
- });
73
-
74
- if (res.status >= 400) {
75
- throw new Error(`Failed to start game (HTTP ${res.status})`);
76
- }
77
-
78
- const html = res.data;
79
- const $ = cheerio.load(html);
80
-
81
- // session + signature live in inline JS / hidden inputs
82
- const sessionMatch = html.match(/#session['"]?\s*\)\s*\.val\(\s*['"]?(\d+)['"]?\s*\)/)
83
- || html.match(/name=['"]session['"][^>]*value=['"](\d+)['"]/);
84
- const signatureMatch = html.match(/#signature['"]?\s*\)\s*\.val\(\s*['"]?(\d+)['"]?\s*\)/)
85
- || html.match(/name=['"]signature['"][^>]*value=['"](\d+)['"]/);
86
-
87
- if (!sessionMatch || !signatureMatch) {
88
- throw new Error('Akinator did not return a valid session. The site layout may have changed.');
89
- }
90
-
91
- this.session = sessionMatch[1];
92
- this.signature = signatureMatch[1];
93
- this.step = 0;
94
- this.progression = '0.00000';
95
- this.guess = null;
96
- this.guessed = false;
97
- this.finished = false;
98
-
99
- const q = $('#question-label').text().trim() || $('.question-text').text().trim();
100
- this.question = q || null;
101
- return { question: this.question, step: this.step, progression: this.progression };
102
- }
103
-
104
- _normalizeAnswer(a) {
105
- if (typeof a === 'number') {
106
- if (a < 0 || a > 4) throw new Error('Answer must be 0-4');
107
- return a;
108
- }
109
- const k = String(a).toLowerCase().trim();
110
- if (!(k in ANSWER_MAP)) {
111
- throw new Error(`Unknown answer "${a}". Use y / n / idk / p / pn or 0-4.`);
112
- }
113
- return ANSWER_MAP[k];
114
- }
115
-
116
- /** Answer the current question. */
117
- async answer(ans) {
118
- if (!this.session) throw new Error('Call start() first.');
119
- if (this.finished) throw new Error('Game already finished.');
120
-
121
- const answer = this._normalizeAnswer(ans);
122
-
123
- const body = new URLSearchParams({
124
- step: String(this.step),
125
- progression: String(this.progression),
126
- sid: '1',
127
- cm: this.childMode ? 'true' : 'false',
128
- answer: String(answer),
129
- step_last_proposition: '',
130
- session: this.session,
131
- signature: this.signature
132
- }).toString();
133
-
134
- const res = await this._client.post('/answer', body, {
135
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
136
- });
137
-
138
- if (res.status >= 400) {
139
- throw new Error(`Answer failed (HTTP ${res.status})`);
140
- }
141
-
142
- const data = typeof res.data === 'string' ? safeJSON(res.data) : res.data;
143
- if (!data) throw new Error('Invalid response from Akinator.');
144
-
145
- // Akinator decided to guess
146
- if (data.id_proposition || data.id_base_proposition) {
147
- this.guess = {
148
- id: data.id_proposition || data.id_base_proposition,
149
- name: data.name_proposition,
150
- description: data.description_proposition,
151
- image: data.photo
152
- };
153
- this.guessed = true;
154
- return { guess: this.guess };
155
- }
156
-
157
- this.step = parseInt(data.step ?? this.step + 1, 10);
158
- this.progression = String(data.progression ?? this.progression);
159
- this.question = data.question || null;
160
-
161
- return {
162
- question: this.question,
163
- step: this.step,
164
- progression: this.progression
165
- };
166
- }
167
-
168
- /** Undo the last answer. */
169
- async back() {
170
- if (!this.session) throw new Error('Call start() first.');
171
- if (this.step <= 0) throw new Error('No previous question.');
172
-
173
- const body = new URLSearchParams({
174
- step: String(this.step),
175
- progression: String(this.progression),
176
- sid: '1',
177
- cm: this.childMode ? 'true' : 'false',
178
- session: this.session,
179
- signature: this.signature
180
- }).toString();
181
-
182
- const res = await this._client.post('/cancel_answer', body, {
183
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
184
- });
185
-
186
- const data = typeof res.data === 'string' ? safeJSON(res.data) : res.data;
187
- if (!data) throw new Error('Invalid response from Akinator.');
188
-
189
- this.step = parseInt(data.step ?? Math.max(0, this.step - 1), 10);
190
- this.progression = String(data.progression ?? this.progression);
191
- this.question = data.question || this.question;
192
-
193
- return {
194
- question: this.question,
195
- step: this.step,
196
- progression: this.progression
197
- };
198
- }
199
-
200
- /** Tell Akinator the guess was wrong and continue playing. */
201
- async continue() {
202
- if (!this.guessed) throw new Error('No active guess to reject.');
203
- const body = new URLSearchParams({
204
- step: String(this.step),
205
- progression: String(this.progression),
206
- sid: '1',
207
- cm: this.childMode ? 'true' : 'false',
208
- session: this.session,
209
- signature: this.signature
210
- }).toString();
211
-
212
- const res = await this._client.post('/exclude', body, {
213
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
214
- });
215
-
216
- const data = typeof res.data === 'string' ? safeJSON(res.data) : res.data;
217
- if (!data) throw new Error('Invalid response from Akinator.');
218
-
219
- this.guess = null;
220
- this.guessed = false;
221
- this.step = parseInt(data.step ?? this.step + 1, 10);
222
- this.progression = String(data.progression ?? this.progression);
223
- this.question = data.question || null;
224
-
225
- return {
226
- question: this.question,
227
- step: this.step,
228
- progression: this.progression
229
- };
230
- }
231
-
232
- /** Confirm the guess was correct (ends the game). */
233
- async win() {
234
- this.finished = true;
235
- return { winner: this.guess };
236
- }
237
- }
238
-
239
- function safeJSON(s) {
240
- try { return JSON.parse(s); } catch { return null; }
241
- }
242
-
243
- module.exports = { Akinator, REGIONS, ANSWER_MAP };
244
- module.exports.default = Akinator;