silent-akinator-pro 2.0.1 → 4.0.3

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,15 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Silent Tech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
package/README.md CHANGED
@@ -1,52 +1,98 @@
1
1
  # silent-akinator-pro
2
2
 
3
- > **Silent Tech** — a modern, working Akinator API wrapper for Node.js.
4
-
5
3
  [![npm](https://img.shields.io/npm/v/silent-akinator-pro.svg)](https://www.npmjs.com/package/silent-akinator-pro)
4
+ [![downloads](https://img.shields.io/npm/dm/silent-akinator-pro.svg)](https://www.npmjs.com/package/silent-akinator-pro)
5
+ [![license](https://img.shields.io/npm/l/silent-akinator-pro.svg)](LICENSE)
6
+
7
+ > **The working Akinator API for Node.js — the only akinator wrapper that still works in 2025 / 2026** after Cloudflare blocked the legacy `api-en4.akinator.com/ws/*` endpoints. Fast, zero-config, Promise-based, fully typed. By **Silent Tech**.
8
+
9
+ The genie / twenty-questions / character-guessing SDK for **Discord bots, Telegram bots, WhatsApp bots, web games, and CLIs**.
10
+
11
+ ## Why this package
12
+
13
+ Most akinator npm packages (`aki-api`, `akinator-api-node`, older `silent-akinator`, etc.) hit JSON endpoints that now return **HTTP 403 / Cloudflare Just-a-moment**. This package uses the **real HTML form flow** the actual website uses — `POST /game`, `/answer`, `/cancel_answer`, `/exclude`, `/list`, `/choice` — no TLS spoofing, no proxies, no `curl-impersonate`. Just clean `undici` + `cheerio`.
14
+
15
+ - Working in 2025 / 2026 (Cloudflare-safe)
16
+ - 22 regions including `en`, `en_objects`, `en_animals`, `fr`, `de`, `es`, `it`, `pt`, `ru`, `ar`, `jp`, `kr`, `cn`, `nl`, `pl`, `tr`, `il`, `id`
17
+ - Child mode supported
18
+ - Full game lifecycle: `start`, `answer`, `back`, `win`, `exclude`, `choice`
19
+ - TypeScript types included
20
+ - Tiny: only `undici` + `cheerio`
21
+ - MIT licensed
6
22
 
7
23
  ## Install
8
24
 
9
25
  ```bash
10
- npm install silent-akinator-pro
26
+ npm i silent-akinator-pro
11
27
  ```
12
28
 
13
- ## Quick Start
29
+ ## Usage
14
30
 
15
31
  ```js
16
- const { Akinator } = require('silent-akinator-pro');
32
+ const Akinator = require('silent-akinator-pro');
17
33
 
18
34
  (async () => {
19
35
  const aki = new Akinator({ region: 'en', childMode: false });
20
36
  await aki.start();
37
+ console.log(aki.question); // first question
38
+
39
+ // 0=yes 1=no 2=idk 3=probably 4=probably-not
40
+ // (or 'y' / 'n' / 'idk' / 'p' / 'pn')
41
+ const r = await aki.answer('y');
42
+ console.log(r.question, r.progression);
21
43
 
22
- console.log(aki.question);
23
- await aki.answer('y'); // y / n / idk / p / pn
24
- console.log(aki.question);
44
+ while (!aki.guessed && aki.step < 80) {
45
+ await aki.answer('idk');
46
+ }
25
47
 
26
- // ...keep answering until aki.guessed === true
27
- if (aki.guess) {
48
+ if (aki.guessed) {
28
49
  console.log('Akinator thinks of:', aki.guess.name);
50
+ await aki.choice(); // confirm
29
51
  }
30
52
  })();
31
53
  ```
32
54
 
33
55
  ## API
34
56
 
35
- ### `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
38
-
39
- ### 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 |
57
+ | Method | Description |
58
+ |---------------|----------------------------------------------|
59
+ | `start()` | Begin a new game; returns first question |
60
+ | `answer(a)` | Submit answer (`0..4` or `y/n/idk/p/pn`) |
61
+ | `back()` | Undo last answer |
62
+ | `win()` | Force Akinator to guess now |
63
+ | `exclude()` | Reject the guess and continue |
64
+ | `choice()` | Confirm the guess (close session) |
47
65
 
48
66
  ### Properties
49
- `question`, `step`, `progression`, `guess`, `guessed`, `finished`.
67
+ `region`, `childMode`, `question`, `step`, `progression`, `guess`, `guessed`, `finished`.
68
+
69
+ ## Discord bot example
70
+
71
+ ```js
72
+ const Akinator = require('silent-akinator-pro');
73
+ const games = new Map();
74
+
75
+ client.on('messageCreate', async (msg) => {
76
+ if (msg.content === '!aki') {
77
+ const aki = new Akinator({ region: 'en' });
78
+ await aki.start();
79
+ games.set(msg.author.id, aki);
80
+ return msg.reply(aki.question);
81
+ }
82
+ 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) return msg.reply(`Is it **${aki.guess.name}**? (${aki.guess.description})`);
87
+ msg.reply(aki.question);
88
+ }
89
+ });
90
+ ```
91
+
92
+ ## Keywords
93
+
94
+ akinator, akinator api, working akinator, akinator 2025, akinator 2026, akinator cloudflare bypass, akinator 403 fix, akinator discord bot, akinator telegram bot, akinator whatsapp bot, twenty questions, character guessing game, genie sdk, node akinator client, aki-api alternative, silent tech, silent-akinator, silent-akinator-pro.
50
95
 
51
96
  ## License
52
- MIT © Silent Tech
97
+
98
+ MIT (c) Silent Tech
package/index.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ export type Region =
2
+ | 'en' | 'en2' | 'en_objects' | 'en_animals' | 'ar' | 'cn' | 'de' | 'es'
3
+ | 'fr' | 'fr_objects' | 'fr_animals' | 'il' | 'it' | 'jp' | 'jp_animals'
4
+ | 'kr' | 'nl' | 'pl' | 'pt' | 'ru' | 'tr' | 'id';
5
+
6
+ export type Answer = 0 | 1 | 2 | 3 | 4 | 'y' | 'n' | 'idk' | 'p' | 'pn'
7
+ | 'yes' | 'no' | 'probably' | 'probably not';
8
+
9
+ export interface AkinatorOptions { region?: Region; childMode?: boolean; }
10
+
11
+ export interface Question {
12
+ question: string;
13
+ step: number;
14
+ progression: string;
15
+ question_id?: string;
16
+ completion?: string;
17
+ answers?: string[];
18
+ }
19
+
20
+ export interface Guess {
21
+ id?: string;
22
+ name?: string;
23
+ description?: string;
24
+ photo?: string;
25
+ pseudo?: string;
26
+ flag_photo?: string | number;
27
+ }
28
+
29
+ export declare class Akinator {
30
+ constructor(opts?: AkinatorOptions);
31
+ region: Region;
32
+ childMode: boolean;
33
+ session: string | null;
34
+ signature: string | null;
35
+ step: number;
36
+ progression: string;
37
+ question: string | null;
38
+ guessed: boolean;
39
+ guess: Guess | null;
40
+ finished: boolean;
41
+
42
+ start(): Promise<Question>;
43
+ answer(a: Answer): Promise<Question | { guess: Guess; step: number; progression: string }>;
44
+ back(): Promise<Question>;
45
+ win(): Promise<any>;
46
+ exclude(): Promise<Question>;
47
+ choice(): Promise<Guess>;
48
+ }
49
+
50
+ export default Akinator;
51
+ export const REGIONS: Region[];
package/index.js ADDED
@@ -0,0 +1,260 @@
1
+ 'use strict';
2
+ /*!
3
+ * silent-akinator-pro
4
+ * Working Akinator API client (2025/2026) — bypasses Cloudflare 403 by using
5
+ * the real HTML form-POST flow instead of the deprecated /ws JSON endpoints.
6
+ *
7
+ * Answers: 0=yes 1=no 2=idk 3=probably 4=probably-not
8
+ * (c) Silent Tech — MIT
9
+ */
10
+ const { request } = require('undici');
11
+ const { load } = require('cheerio');
12
+
13
+ const REGIONS = [
14
+ 'en','en2','en_objects','en_animals','ar','cn','de','es','fr','fr_objects',
15
+ 'fr_animals','il','it','jp','jp_animals','kr','nl','pl','pt','ru','tr','id',
16
+ ];
17
+
18
+ const ANSWER_MAP = {
19
+ y: 0, yes: 0, '0': 0,
20
+ n: 1, no: 1, '1': 1,
21
+ idk: 2, dk: 2, "don't know": 2, '2': 2,
22
+ p: 3, probably: 3, '3': 3,
23
+ pn: 4, 'probably not': 4, '4': 4,
24
+ };
25
+
26
+ const BASE_HEADERS = {
27
+ 'content-type' : 'application/x-www-form-urlencoded',
28
+ 'user-agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
29
+ 'x-requested-with': 'XMLHttpRequest',
30
+ };
31
+
32
+ const form = (obj) => new URLSearchParams(
33
+ Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, String(v)]))
34
+ ).toString();
35
+
36
+ class Akinator {
37
+ /**
38
+ * @param {object} opts
39
+ * @param {string} [opts.region='en'] one of REGIONS
40
+ * @param {boolean} [opts.childMode=false] safe-mode filter
41
+ */
42
+ constructor(opts = {}) {
43
+ const { region = 'en', childMode = false } = opts;
44
+ if (!REGIONS.includes(region)) {
45
+ throw new Error(`Invalid region "${region}". Valid: ${REGIONS.join(', ')}`);
46
+ }
47
+ this.region = region;
48
+ this.childMode = !!childMode;
49
+ this.uri = `https://${region}.akinator.com`;
50
+ this.headers = { ...BASE_HEADERS };
51
+
52
+ this.session = null;
53
+ this.signature = null;
54
+ this.step = 0;
55
+ this.progression = '0.00000';
56
+ this.question = null;
57
+ this.guessed = false;
58
+ this.guess = null;
59
+ this.finished = false;
60
+ }
61
+
62
+ _answerCode(a) {
63
+ if (typeof a === 'number') a = String(a);
64
+ const code = ANSWER_MAP[String(a).toLowerCase().trim()];
65
+ if (code === undefined) {
66
+ throw new Error(`Invalid answer "${a}". Use y / n / idk / p / pn or 0..4.`);
67
+ }
68
+ return code;
69
+ }
70
+
71
+ /** Start a new game. Resolves with the first question. */
72
+ async start() {
73
+ const res = await request(`${this.uri}/game`, {
74
+ method: 'POST',
75
+ headers: this.headers,
76
+ body: form({ cm: this.childMode, sid: 1 }),
77
+ maxRedirections: 5,
78
+ });
79
+ if (res.statusCode !== 200) {
80
+ throw new Error(`Akinator /game returned ${res.statusCode}`);
81
+ }
82
+ const html = await res.body.text();
83
+ const $ = load(html);
84
+
85
+ this.session = $('#askSoundlike > #session').attr('value')
86
+ || $('#session').attr('value');
87
+ this.signature = $('#askSoundlike > #signature').attr('value')
88
+ || $('#signature').attr('value');
89
+ this.question = $('#question-label').text().trim()
90
+ || $('.bubble-body .question-text').text().trim();
91
+
92
+ if (!this.session || !this.signature) {
93
+ throw new Error('Failed to parse session/signature from Akinator response.');
94
+ }
95
+ this.step = 0;
96
+ this.progression = '0.00000';
97
+ return {
98
+ question: this.question,
99
+ step: this.step,
100
+ progression: this.progression,
101
+ answers: ['yes', 'no', 'idk', 'probably', 'probably not'],
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Submit an answer.
107
+ * @param {0|1|2|3|4|string} a
108
+ */
109
+ async answer(a) {
110
+ if (this.finished) throw new Error('Game already finished.');
111
+ if (!this.session) throw new Error('Call start() first.');
112
+ const ans = this._answerCode(a);
113
+
114
+ const res = await request(`${this.uri}/answer`, {
115
+ method: 'POST',
116
+ headers: this.headers,
117
+ body: form({
118
+ step : this.step,
119
+ progression : this.progression,
120
+ sid : 1,
121
+ cm : this.childMode,
122
+ answer : ans,
123
+ step_last_proposition: '',
124
+ session : this.session,
125
+ signature : this.signature,
126
+ }),
127
+ });
128
+
129
+ let data;
130
+ try { data = JSON.parse(await res.body.text()); }
131
+ catch { throw new Error(`/answer returned non-JSON (status ${res.statusCode}).`); }
132
+
133
+ if (data.id_proposition || data.name_proposition) {
134
+ this.guess = {
135
+ id : data.id_proposition,
136
+ name : data.name_proposition,
137
+ description : data.description_proposition,
138
+ photo : data.photo,
139
+ pseudo : data.pseudo,
140
+ flag_photo : data.flag_photo,
141
+ };
142
+ this.guessed = true;
143
+ return { guess: this.guess, step: Number(this.step), progression: this.progression };
144
+ }
145
+
146
+ this.step = parseInt(data.step ?? this.step + 1, 10);
147
+ this.progression = data.progression || this.progression;
148
+ this.question = data.question || this.question;
149
+ this.guessed = false;
150
+ this.guess = null;
151
+ return {
152
+ question : this.question,
153
+ step : this.step,
154
+ progression : this.progression,
155
+ question_id : data.question_id,
156
+ completion : data.completion,
157
+ };
158
+ }
159
+
160
+ /** Undo the last answer. */
161
+ async back() {
162
+ if (this.step <= 0) throw new Error('Nothing to undo.');
163
+ const res = await request(`${this.uri}/cancel_answer`, {
164
+ method: 'POST',
165
+ headers: this.headers,
166
+ body: form({
167
+ step : this.step,
168
+ progression: this.progression,
169
+ sid : 1,
170
+ cm : this.childMode,
171
+ session : this.session,
172
+ signature : this.signature,
173
+ }),
174
+ });
175
+ const data = JSON.parse(await res.body.text());
176
+ this.step = parseInt(data.step ?? Math.max(0, this.step - 1), 10);
177
+ this.progression = data.progression || this.progression;
178
+ this.question = data.question || this.question;
179
+ this.guessed = false;
180
+ this.guess = null;
181
+ return { question: this.question, step: this.step, progression: this.progression };
182
+ }
183
+
184
+ /** Force Akinator to make its best guess now. */
185
+ async win() {
186
+ const res = await request(`${this.uri}/list`, {
187
+ method: 'POST',
188
+ headers: this.headers,
189
+ body: form({
190
+ step : this.step,
191
+ progression : this.progression,
192
+ sid : 1,
193
+ cm : this.childMode,
194
+ mode_question : 0,
195
+ session : this.session,
196
+ signature : this.signature,
197
+ }),
198
+ });
199
+ const text = await res.body.text();
200
+ let data;
201
+ try { data = JSON.parse(text); }
202
+ catch {
203
+ // /list sometimes returns HTML when progression is too low or session expired.
204
+ // Fall back to current guess or last known state.
205
+ if (this.guess) return this.guess;
206
+ throw new Error(`/list returned non-JSON (status ${res.statusCode}). Try answering more questions before calling win().`);
207
+ }
208
+ this.guessed = true;
209
+ this.guess = data;
210
+ return data;
211
+ }
212
+
213
+ /** Reject the current guess and continue playing. */
214
+ async exclude() {
215
+ if (!this.guessed) throw new Error('No active guess to reject.');
216
+ const res = await request(`${this.uri}/exclude`, {
217
+ method: 'POST',
218
+ headers: this.headers,
219
+ body: form({
220
+ step : this.step,
221
+ progression: this.progression,
222
+ sid : 1,
223
+ cm : this.childMode,
224
+ session : this.session,
225
+ signature : this.signature,
226
+ }),
227
+ });
228
+ const data = JSON.parse(await res.body.text());
229
+ this.step = parseInt(data.step ?? this.step + 1, 10);
230
+ this.progression = data.progression || this.progression;
231
+ this.question = data.question || this.question;
232
+ this.guessed = false;
233
+ this.guess = null;
234
+ return { question: this.question, step: this.step, progression: this.progression };
235
+ }
236
+
237
+ /** Confirm Akinator's guess (closes the session). */
238
+ async choice() {
239
+ if (!this.guess) throw new Error('No guess to confirm.');
240
+ await request(`${this.uri}/choice`, {
241
+ method: 'POST',
242
+ headers: this.headers,
243
+ body: form({
244
+ sid : 1,
245
+ charac_name : this.guess.name || '',
246
+ charac_desc : this.guess.description || '',
247
+ session : this.session,
248
+ signature : this.signature,
249
+ pflag_photo : 0,
250
+ }),
251
+ });
252
+ this.finished = true;
253
+ return this.guess;
254
+ }
255
+ }
256
+
257
+ module.exports = Akinator;
258
+ module.exports.default = Akinator;
259
+ module.exports.Akinator = Akinator;
260
+ module.exports.REGIONS = REGIONS;
package/package.json CHANGED
@@ -1,34 +1,78 @@
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": "4.0.3",
4
+ "description": "Working Akinator API for Node.js (2025/2026). Fast, lightweight, Cloudflare-safe akinator client / wrapper / SDK / bot library \u2014 supports 22 regions, child mode, undo, win, exclude. The genie / 20-questions / character-guessing API that actually works after the api-en4 shutdown. Drop-in replacement for aki-api and akinator-api.",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "type": "commonjs",
8
+ "files": [
9
+ "index.js",
10
+ "index.d.ts",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
7
14
  "scripts": {
8
- "test": "node examples/basic.js"
15
+ "test": "node test.js"
9
16
  },
10
17
  "keywords": [
11
18
  "akinator",
19
+ "akinator-api",
20
+ "akinator-bot",
21
+ "akinator-client",
22
+ "akinator-sdk",
23
+ "akinator-wrapper",
24
+ "akinator-node",
25
+ "akinator-js",
26
+ "akinator-2025",
27
+ "akinator-2026",
28
+ "working-akinator",
29
+ "akinator-fix",
30
+ "akinator-cloudflare",
31
+ "cloudflare-bypass",
32
+ "akinator-403",
33
+ "aki-api",
34
+ "aki",
35
+ "genie",
36
+ "20-questions",
37
+ "twenty-questions",
38
+ "guessing-game",
39
+ "character-guess",
40
+ "ai-guess",
41
+ "game",
42
+ "ai-game",
43
+ "quiz",
44
+ "trivia",
45
+ "fun-api",
46
+ "discord-bot",
47
+ "discord-akinator",
48
+ "telegram-bot",
49
+ "whatsapp-bot",
50
+ "baileys",
51
+ "chatbot",
52
+ "scraper",
53
+ "typescript",
54
+ "esm",
55
+ "cjs",
12
56
  "silent",
13
57
  "silent-tech",
14
58
  "silent-akinator",
15
- "game",
16
- "genie",
17
- "api",
18
- "wrapper"
59
+ "silent-akinator-pro"
19
60
  ],
20
- "author": "Silent Tech",
61
+ "author": "Silent Tech <silent.tech.offc@gmail.com>",
21
62
  "license": "MIT",
22
- "dependencies": {
23
- "axios": "^1.7.7",
24
- "cheerio": "^1.0.0"
25
- },
26
- "engines": {
27
- "node": ">=14"
28
- },
29
63
  "repository": {
30
64
  "type": "git",
31
- "url": "https://github.com/silent-tech/silent-akinator-pro"
65
+ "url": "git+https://github.com/silent-tech/silent-akinator-pro.git"
32
66
  },
33
- "homepage": "https://www.npmjs.com/package/silent-akinator-pro"
67
+ "homepage": "https://github.com/silent-tech/silent-akinator-pro#readme",
68
+ "bugs": {
69
+ "url": "https://github.com/silent-tech/silent-akinator-pro/issues"
70
+ },
71
+ "engines": {
72
+ "node": ">=18"
73
+ },
74
+ "dependencies": {
75
+ "undici": "^6.19.8",
76
+ "cheerio": "^1.0.0"
77
+ }
34
78
  }
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;