whatsapp-chess-bot 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/index.js +196 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# WhatsApp Chess Bot โ๏ธ
|
|
2
|
+
|
|
3
|
+
A WhatsApp bot that lets you play chess against an AI opponent using interactive polls. The bot displays the chess board as images and uses polls for move selection.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ๐ฎ Play chess via WhatsApp polls
|
|
8
|
+
- ๐ค AI opponent powered by js-chess-engine
|
|
9
|
+
- ๐ผ๏ธ Visual chess board using chessboardimage.com
|
|
10
|
+
- ๐งน Automatic message cleanup (keeps only last 2 board images)
|
|
11
|
+
- โ๏ธ Choose piece type, then select specific move
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install whatsapp-chess-bot
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or clone and run:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone <your-repo-url>
|
|
23
|
+
cd whatsapp-chess-bot
|
|
24
|
+
npm install
|
|
25
|
+
npm start
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
1. Run the bot:
|
|
31
|
+
```bash
|
|
32
|
+
npm start
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
2. Scan the QR code with WhatsApp (Link Device)
|
|
36
|
+
|
|
37
|
+
3. Bot is ready! Send commands in any WhatsApp chat.
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
- `@chess start` - Start a new chess game
|
|
42
|
+
- `@chess clean` - Delete all bot images from chat
|
|
43
|
+
|
|
44
|
+
## How to Play
|
|
45
|
+
|
|
46
|
+
1. Send `@chess start` in any WhatsApp chat
|
|
47
|
+
2. The bot shows the current board position
|
|
48
|
+
3. Vote in the "Choose a piece type" poll (Pawn, Knight, Bishop, etc.)
|
|
49
|
+
4. Vote in the move poll to select your piece's destination
|
|
50
|
+
5. The AI responds automatically
|
|
51
|
+
6. Repeat until checkmate or stalemate
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
Edit `index.js` to customize:
|
|
56
|
+
|
|
57
|
+
- **AI Difficulty**: Change `engine.aiMove(gameData.game.fen(), 2)` - the `2` is the search depth (higher = stronger)
|
|
58
|
+
- **Max Images**: Change `enforceMaxImages(chatId, 2)` - default keeps 2 board images
|
|
59
|
+
- **Session Name**: Change `clientId: "chess-poll-session"` for multiple instances
|
|
60
|
+
|
|
61
|
+
## Requirements
|
|
62
|
+
|
|
63
|
+
- Node.js 14 or higher
|
|
64
|
+
- WhatsApp account
|
|
65
|
+
- Internet connection
|
|
66
|
+
|
|
67
|
+
## Dependencies
|
|
68
|
+
|
|
69
|
+
- `whatsapp-web.js` - WhatsApp client
|
|
70
|
+
- `qrcode-terminal` - QR code display
|
|
71
|
+
- `chess.js` - Chess game logic
|
|
72
|
+
- `js-chess-engine` - AI opponent
|
|
73
|
+
|
|
74
|
+
## Notes
|
|
75
|
+
|
|
76
|
+
- WhatsApp may prevent deletion of old messages depending on timing/permissions
|
|
77
|
+
- For best experience, enable disappearing messages in the chat
|
|
78
|
+
- Board images are fetched from https://chessboardimage.com/
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
const { Client, LocalAuth, MessageMedia, Poll } = require('whatsapp-web.js');
|
|
2
|
+
const qrcode = require('qrcode-terminal');
|
|
3
|
+
const { Chess } = require('chess.js');
|
|
4
|
+
const engine = require('js-chess-engine');
|
|
5
|
+
|
|
6
|
+
const client = new Client({
|
|
7
|
+
authStrategy: new LocalAuth({ clientId: "chess-poll-session" }),
|
|
8
|
+
webVersionCache: {
|
|
9
|
+
type: 'remote',
|
|
10
|
+
remotePath: 'https://raw.githubusercontent.com/wppconnect-team/wa-version/main/html/2.2412.54.html',
|
|
11
|
+
},
|
|
12
|
+
puppeteer: { args: ['--no-sandbox', '--disable-setuid-sandbox'] }
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const activeChess = {};
|
|
16
|
+
const PIECE_NAMES = { 'p': 'Pawn โ๏ธ', 'n': 'Knight โ', 'b': 'Bishop โ', 'r': 'Rook โ', 'q': 'Queen โ', 'k': 'King โ' };
|
|
17
|
+
|
|
18
|
+
client.on('qr', (qr) => qrcode.generate(qr, { small: true }));
|
|
19
|
+
client.on('ready', () => console.log('โ๏ธ Chess Poll Bot is online!'));
|
|
20
|
+
|
|
21
|
+
client.on('message_create', async (msg) => {
|
|
22
|
+
if (msg.fromMe) return;
|
|
23
|
+
const chatId = msg.from;
|
|
24
|
+
const body = msg.body.toLowerCase().trim();
|
|
25
|
+
|
|
26
|
+
if (body === '@chess clean') {
|
|
27
|
+
await deletePastImages(chatId);
|
|
28
|
+
await client.sendMessage(chatId, '๐งน Deleted recent bot images.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (body === '@chess start') {
|
|
33
|
+
activeChess[chatId] = {
|
|
34
|
+
game: new Chess(),
|
|
35
|
+
lastPollId: null,
|
|
36
|
+
phase: 'piece_select',
|
|
37
|
+
trackedMessageIds: [],
|
|
38
|
+
lastPiecePollId: null,
|
|
39
|
+
imageMessageIds: []
|
|
40
|
+
};
|
|
41
|
+
await deletePastImages(chatId);
|
|
42
|
+
await client.sendMessage(chatId, "โ๏ธ *Chess Started!* Use the polls to play.");
|
|
43
|
+
await sendChessBoard(chatId);
|
|
44
|
+
await sendPiecePoll(chatId);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
client.on('vote_update', async (vote) => {
|
|
49
|
+
const chatId = vote.parentMessage.to;
|
|
50
|
+
const gameData = activeChess[chatId];
|
|
51
|
+
if (!gameData || vote.parentMessage.id._serialized !== gameData.lastPollId) return;
|
|
52
|
+
|
|
53
|
+
const selection = vote.selectedOptions[0]?.name;
|
|
54
|
+
if (!selection) return;
|
|
55
|
+
|
|
56
|
+
if (gameData.phase === 'piece_select') {
|
|
57
|
+
const pieceType = Object.keys(PIECE_NAMES).find(k => PIECE_NAMES[k] === selection);
|
|
58
|
+
await sendMovePoll(chatId, pieceType);
|
|
59
|
+
} else if (gameData.phase === 'move_select') {
|
|
60
|
+
await executeMove(chatId, selection);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
async function sendPiecePoll(chatId) {
|
|
65
|
+
const gameData = activeChess[chatId];
|
|
66
|
+
if (gameData.lastPiecePollId) {
|
|
67
|
+
await deleteMessageById(gameData.lastPiecePollId);
|
|
68
|
+
gameData.lastPiecePollId = null;
|
|
69
|
+
}
|
|
70
|
+
const moves = gameData.game.moves({ verbose: true });
|
|
71
|
+
const pieces = [...new Set(moves.map(m => m.piece))].map(p => PIECE_NAMES[p]);
|
|
72
|
+
const poll = new Poll('Choose a piece type:', pieces);
|
|
73
|
+
const sent = await client.sendMessage(chatId, poll);
|
|
74
|
+
gameData.lastPiecePollId = sent.id._serialized;
|
|
75
|
+
gameData.lastPollId = sent.id._serialized;
|
|
76
|
+
gameData.phase = 'piece_select';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function sendMovePoll(chatId, pieceType) {
|
|
80
|
+
const gameData = activeChess[chatId];
|
|
81
|
+
await deleteTrackedMessages(chatId);
|
|
82
|
+
if (gameData.lastPiecePollId) {
|
|
83
|
+
await deleteMessageById(gameData.lastPiecePollId);
|
|
84
|
+
gameData.lastPiecePollId = null;
|
|
85
|
+
}
|
|
86
|
+
const moves = gameData.game.moves({ verbose: true }).filter(m => m.piece === pieceType).map(m => m.san);
|
|
87
|
+
const poll = new Poll(`Move ${PIECE_NAMES[pieceType]} to:`, moves.slice(0, 12));
|
|
88
|
+
const sent = await sendTrackedMessage(chatId, poll);
|
|
89
|
+
gameData.lastPollId = sent.id._serialized;
|
|
90
|
+
gameData.phase = 'move_select';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function executeMove(chatId, moveSan) {
|
|
94
|
+
const gameData = activeChess[chatId];
|
|
95
|
+
await deleteTrackedMessages(chatId);
|
|
96
|
+
if (gameData.lastPollId) {
|
|
97
|
+
await deleteMessageById(gameData.lastPollId);
|
|
98
|
+
}
|
|
99
|
+
gameData.game.move(moveSan);
|
|
100
|
+
let computerPlayedSan = null;
|
|
101
|
+
if (!gameData.game.isGameOver()) {
|
|
102
|
+
const ai = engine.aiMove(gameData.game.fen(), 2);
|
|
103
|
+
const from = Object.keys(ai)[0].toLowerCase();
|
|
104
|
+
const to = ai[Object.keys(ai)[0]].toLowerCase();
|
|
105
|
+
const aiMove = gameData.game.move({ from, to, promotion: 'q' });
|
|
106
|
+
computerPlayedSan = aiMove?.san || `${from}-${to}`;
|
|
107
|
+
}
|
|
108
|
+
if (computerPlayedSan) {
|
|
109
|
+
await sendTrackedMessage(chatId, `๐ค Computer played: *${computerPlayedSan}*`);
|
|
110
|
+
}
|
|
111
|
+
await sendChessBoard(chatId);
|
|
112
|
+
if (gameData.game.isGameOver()) {
|
|
113
|
+
await client.sendMessage(chatId, "๐ *Game Over!*");
|
|
114
|
+
delete activeChess[chatId];
|
|
115
|
+
} else {
|
|
116
|
+
await sendPiecePoll(chatId);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function sendChessBoard(chatId) {
|
|
121
|
+
const gameData = activeChess[chatId];
|
|
122
|
+
const fen = gameData.game.fen();
|
|
123
|
+
const fenUrl = encodeURI(fen); // Keep '/' separators, encode spaces
|
|
124
|
+
console.log(fen);
|
|
125
|
+
const media = await MessageMedia.fromUrl(`https://chessboardimage.com/${fenUrl}.png`);
|
|
126
|
+
const sent = await client.sendMessage(chatId, media);
|
|
127
|
+
gameData.imageMessageIds ??= [];
|
|
128
|
+
gameData.imageMessageIds.push(sent.id._serialized);
|
|
129
|
+
await enforceMaxImages(chatId, 2);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function sendTrackedMessage(chatId, content, track = true) {
|
|
133
|
+
const sent = await client.sendMessage(chatId, content);
|
|
134
|
+
const gameData = activeChess[chatId];
|
|
135
|
+
if (gameData && track) {
|
|
136
|
+
gameData.trackedMessageIds ??= [];
|
|
137
|
+
gameData.trackedMessageIds.push(sent.id._serialized);
|
|
138
|
+
}
|
|
139
|
+
return sent;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function deleteTrackedMessages(chatId) {
|
|
143
|
+
const gameData = activeChess[chatId];
|
|
144
|
+
if (!gameData?.trackedMessageIds?.length) return;
|
|
145
|
+
|
|
146
|
+
const idsToDelete = [...gameData.trackedMessageIds];
|
|
147
|
+
gameData.trackedMessageIds = [];
|
|
148
|
+
|
|
149
|
+
for (const id of idsToDelete) {
|
|
150
|
+
try {
|
|
151
|
+
const message = await client.getMessageById(id);
|
|
152
|
+
if (message) await message.delete(true);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.log(`Could not delete message ${id}:`, err.message);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function deletePastImages(chatId) {
|
|
160
|
+
try {
|
|
161
|
+
const chat = await client.getChatById(chatId);
|
|
162
|
+
const recent = await chat.fetchMessages({ limit: 200 });
|
|
163
|
+
for (const msg of recent) {
|
|
164
|
+
if (!msg.fromMe || !msg.hasMedia) continue;
|
|
165
|
+
try {
|
|
166
|
+
await msg.delete(true);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.log(`Could not delete old image ${msg.id?._serialized}:`, err.message);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.log('Could not fetch chat for image cleanup:', err.message);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function enforceMaxImages(chatId, maxImages = 2) {
|
|
177
|
+
const gameData = activeChess[chatId];
|
|
178
|
+
if (!gameData) return;
|
|
179
|
+
|
|
180
|
+
gameData.imageMessageIds ??= [];
|
|
181
|
+
while (gameData.imageMessageIds.length > maxImages) {
|
|
182
|
+
const oldestId = gameData.imageMessageIds.shift();
|
|
183
|
+
if (oldestId) await deleteMessageById(oldestId);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function deleteMessageById(messageId) {
|
|
188
|
+
try {
|
|
189
|
+
const message = await client.getMessageById(messageId);
|
|
190
|
+
if (message) await message.delete(true);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.log(`Could not delete message ${messageId}:`, err.message);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
client.initialize();
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "whatsapp-chess-bot",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A WhatsApp bot that lets you play chess via interactive polls with an AI opponent",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node index.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"whatsapp",
|
|
11
|
+
"chess",
|
|
12
|
+
"bot",
|
|
13
|
+
"poll",
|
|
14
|
+
"game",
|
|
15
|
+
"ai"
|
|
16
|
+
],
|
|
17
|
+
"author": "David Sebbag",
|
|
18
|
+
"license": "GPL-3.0",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"whatsapp-web.js": "^1.23.0",
|
|
21
|
+
"qrcode-terminal": "^0.12.0",
|
|
22
|
+
"chess.js": "^1.0.0",
|
|
23
|
+
"js-chess-engine": "^1.0.2"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=14.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|