From a13780d3bad2bda0b69f02cb884f334a4067b080 Mon Sep 17 00:00:00 2001 From: konard Date: Fri, 17 Oct 2025 20:16:03 +0300 Subject: [PATCH 1/3] Initial commit with task details for issue #26 Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: undefined --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6af1f36 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: undefined +Your prepared branch: issue-26-f4950b82 +Your prepared working directory: /tmp/gh-issue-solver-1760721360375 + +Proceed. \ No newline at end of file From 8872d8211c5dd212fa360a8a797197396035d5f9 Mon Sep 17 00:00:00 2001 From: konard Date: Fri, 17 Oct 2025 20:21:37 +0300 Subject: [PATCH 2/3] Add 2-4-6 game support for private messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements an interactive 2-4-6 puzzle game that can be played via VK private messages. The game teaches inductive reasoning and helps users understand confirmation bias. Game Features: - Start game with commands like "играть 246", "play", "сыграем" - Test number triples to discover the hidden rule - Submit rule guesses (limited to one per day) - Bilingual support (Russian and English) - The rule: numbers must be in ascending order (a < b < c) Implementation: - Three triggers handle different game phases: 1. startGameTrigger: Initializes new game sessions 2. handleTripleGuessTrigger: Validates number triples 3. handleRuleGuessTrigger: Evaluates rule guesses - Game state tracked per user in peer state object - Comprehensive test coverage with Jest - Experiment script for manual testing Fixes #26 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- __tests__/triggers/two-four-six-game.js | 199 +++++++++++++++++++ experiments/test-two-four-six-game.js | 96 ++++++++++ index.js | 3 + triggers/two-four-six-game.js | 243 ++++++++++++++++++++++++ 4 files changed, 541 insertions(+) create mode 100644 __tests__/triggers/two-four-six-game.js create mode 100644 experiments/test-two-four-six-game.js create mode 100644 triggers/two-four-six-game.js diff --git a/__tests__/triggers/two-four-six-game.js b/__tests__/triggers/two-four-six-game.js new file mode 100644 index 0000000..1a0cfb4 --- /dev/null +++ b/__tests__/triggers/two-four-six-game.js @@ -0,0 +1,199 @@ +const { + startGameTrigger, + handleTripleGuessTrigger, + handleRuleGuessTrigger, + checkRule, +} = require('../../triggers/two-four-six-game'); +const { enqueueMessage } = require('../../outgoing-messages'); +jest.mock('../../outgoing-messages'); + +describe('2-4-6 game triggers', () => { + beforeEach(() => { + enqueueMessage.mockClear(); + }); + + describe('startGameTrigger', () => { + test.each([ + ['играть 246'], + ['играть'], + ['игра 2 4 6'], + ['давай игра'], + ['play 246'], + ["let's play"], + ['сыграем'], + ])('"%s" message starts the game', (incomingMessage) => { + const context = { + request: { peerType: 'user', text: incomingMessage }, + state: {} + }; + expect(startGameTrigger.condition(context)).toBe(true); + startGameTrigger.action(context); + expect(enqueueMessage).toHaveBeenCalled(); + expect(context.state.games.twoFourSixGame.active).toBe(true); + }); + + test.each([ + ['привет'], + ['2 4 6'], + ['как дела?'], + ])('"%s" message does not start the game', (incomingMessage) => { + const context = { + request: { peerType: 'user', text: incomingMessage }, + state: {} + }; + expect(startGameTrigger.condition(context)).toBe(false); + }); + + test('does not trigger for outgoing messages', () => { + const context = { + request: { peerType: 'user', text: 'играть', isOutbox: true }, + state: {} + }; + expect(startGameTrigger.condition(context)).toBe(false); + }); + }); + + describe('handleTripleGuessTrigger', () => { + test.each([ + ['2 4 6', [2, 4, 6], true], + ['8, 10, 12', [8, 10, 12], true], + ['-1, 121, 130.5', [-1, 121, 130.5], true], + ['100 200 300', [100, 200, 300], true], + ['2 2 3', [2, 2, 3], false], + ['3 2 1', [3, 2, 1], false], + ])('"%s" triple is processed correctly', (incomingMessage, triple, shouldMatch) => { + const context = { + request: { peerType: 'user', text: incomingMessage }, + state: { + games: { + twoFourSixGame: { + active: true, + guesses: [] + } + } + } + }; + expect(handleTripleGuessTrigger.condition(context)).toBe(true); + handleTripleGuessTrigger.action(context); + expect(enqueueMessage).toHaveBeenCalled(); + const callArg = enqueueMessage.mock.calls[0][0]; + expect(callArg.response.message).toContain(shouldMatch ? '✅' : '❌'); + expect(context.state.games.twoFourSixGame.guesses.length).toBe(1); + expect(context.state.games.twoFourSixGame.guesses[0].triple).toEqual(triple); + }); + + test('does not trigger when game is not active', () => { + const context = { + request: { peerType: 'user', text: '2 4 6' }, + state: { + games: { + twoFourSixGame: { + active: false + } + } + } + }; + expect(handleTripleGuessTrigger.condition(context)).toBe(false); + }); + }); + + describe('handleRuleGuessTrigger', () => { + test('accepts correct rule guess with ascending keywords', () => { + const context = { + request: { peerType: 'user', text: 'правило: числа в порядке возрастания' }, + state: { + games: { + twoFourSixGame: { + active: true, + guesses: [], + ruleGuessesCount: 0 + } + } + } + }; + expect(handleRuleGuessTrigger.condition(context)).toBe(true); + handleRuleGuessTrigger.action(context); + expect(enqueueMessage).toHaveBeenCalled(); + const callArg = enqueueMessage.mock.calls[0][0]; + expect(callArg.response.message).toContain('🎉'); + expect(context.state.games.twoFourSixGame.active).toBe(false); + }); + + test('rejects incorrect rule guess', () => { + const context = { + request: { peerType: 'user', text: 'правило: четные числа' }, + state: { + games: { + twoFourSixGame: { + active: true, + guesses: [], + ruleGuessesCount: 0 + } + } + } + }; + expect(handleRuleGuessTrigger.condition(context)).toBe(true); + handleRuleGuessTrigger.action(context); + expect(enqueueMessage).toHaveBeenCalled(); + const callArg = enqueueMessage.mock.calls[0][0]; + expect(callArg.response.message).toContain('❌'); + expect(context.state.games.twoFourSixGame.active).toBe(true); + }); + + test('prevents multiple rule guesses per day', () => { + const yesterday = new Date(); + yesterday.setHours(yesterday.getHours() - 12); + + const context = { + request: { peerType: 'user', text: 'правило: числа больше' }, + state: { + games: { + twoFourSixGame: { + active: true, + guesses: [], + ruleGuessesCount: 1, + lastRuleGuessDate: yesterday + } + } + } + }; + expect(handleRuleGuessTrigger.condition(context)).toBe(true); + handleRuleGuessTrigger.action(context); + expect(enqueueMessage).toHaveBeenCalled(); + const callArg = enqueueMessage.mock.calls[0][0]; + expect(callArg.response.message).toContain('⏰'); + }); + + test('does not trigger for triple guesses', () => { + const context = { + request: { peerType: 'user', text: '2 4 6' }, + state: { + games: { + twoFourSixGame: { + active: true, + guesses: [] + } + } + } + }; + expect(handleRuleGuessTrigger.condition(context)).toBe(false); + }); + }); + + describe('checkRule', () => { + test.each([ + [2, 4, 6, true], + [8, 10, 12, true], + [-1, 121, 130.5, true], + [1, 2, 3, true], + [0, 0.5, 1, true], + [2, 2, 3, false], + [3, 2, 1, false], + [6, 4, 2, false], + [1, 1, 1, false], + [5, 3, 7, false], + ])('checkRule(%d, %d, %d) returns %s', (a, b, c, expected) => { + expect(checkRule(a, b, c)).toBe(expected); + }); + }); +}); diff --git a/experiments/test-two-four-six-game.js b/experiments/test-two-four-six-game.js new file mode 100644 index 0000000..3899e5d --- /dev/null +++ b/experiments/test-two-four-six-game.js @@ -0,0 +1,96 @@ +// Test script for the 2-4-6 game triggers +const { + startGameRegex, + tripleRegex, + ruleGuessRegex, + checkRule, +} = require('../triggers/two-four-six-game'); + +console.log('Testing 2-4-6 game implementation...\n'); + +// Test 1: Start game regex +console.log('=== Test 1: Start Game Regex ==='); +const startGameTests = [ + { text: 'играть 246', expected: true }, + { text: 'играть', expected: true }, + { text: 'игра 2 4 6', expected: true }, + { text: 'давай игра', expected: true }, + { text: 'play 246', expected: true }, + { text: 'play game', expected: true }, + { text: "let's play", expected: true }, + { text: 'сыграем', expected: true }, + { text: 'привет', expected: false }, + { text: '2 4 6', expected: false }, +]; + +startGameTests.forEach(test => { + const result = startGameRegex.test(test.text); + const status = result === test.expected ? '✅' : '❌'; + console.log(`${status} "${test.text}" -> ${result} (expected: ${test.expected})`); +}); + +// Test 2: Triple regex +console.log('\n=== Test 2: Triple Regex ==='); +const tripleTests = [ + { text: '2 4 6', expected: true }, + { text: '8, 10, 12', expected: true }, + { text: '-1, 121, 130.5', expected: true }, + { text: '100 200 300', expected: true }, + { text: '2.5, 3.7, 4.9', expected: true }, + { text: '2 4', expected: false }, + { text: '1 2 3 4', expected: false }, + { text: 'привет', expected: false }, +]; + +tripleTests.forEach(test => { + const result = tripleRegex.test(test.text); + const status = result === test.expected ? '✅' : '❌'; + console.log(`${status} "${test.text}" -> ${result} (expected: ${test.expected})`); + if (result) { + const match = test.text.match(tripleRegex); + console.log(` Extracted: [${match[1]}, ${match[2]}, ${match[3]}]`); + } +}); + +// Test 3: Rule guess regex +console.log('\n=== Test 3: Rule Guess Regex ==='); +const ruleGuessTests = [ + { text: 'правило: числа возрастают', expected: true }, + { text: 'я знаю правило', expected: true }, + { text: 'понял! числа должны расти', expected: true }, + { text: 'rule: ascending order', expected: true }, + { text: 'i know the rule', expected: true }, + { text: 'got it: numbers increase', expected: true }, + { text: 'числа должны идти в порядке возрастания, каждое следующее больше предыдущего', expected: true }, + { text: '2 4 6', expected: false }, +]; + +ruleGuessTests.forEach(test => { + const result = ruleGuessRegex.test(test.text) || test.text.length > 30; + const status = result === test.expected ? '✅' : '❌'; + console.log(`${status} "${test.text}" -> ${result} (expected: ${test.expected})`); +}); + +// Test 4: Check rule function +console.log('\n=== Test 4: Check Rule Function ==='); +const ruleTests = [ + { triple: [2, 4, 6], expected: true }, + { triple: [8, 10, 12], expected: true }, + { triple: [-1, 121, 130.5], expected: true }, + { triple: [1, 2, 3], expected: true }, + { triple: [0, 0.5, 1], expected: true }, + { triple: [2, 2, 3], expected: false }, + { triple: [3, 2, 1], expected: false }, + { triple: [6, 4, 2], expected: false }, + { triple: [1, 1, 1], expected: false }, + { triple: [5, 3, 7], expected: false }, +]; + +ruleTests.forEach(test => { + const [a, b, c] = test.triple; + const result = checkRule(a, b, c); + const status = result === test.expected ? '✅' : '❌'; + console.log(`${status} checkRule(${a}, ${b}, ${c}) -> ${result} (expected: ${test.expected})`); +}); + +console.log('\n=== All Tests Complete ==='); diff --git a/index.js b/index.js index 4226830..c2f483d 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,9 @@ const triggers = [ // require('./triggers/who-singular').trigger, // require('./triggers/have-we-talked-before').trigger, // require('./triggers/engage-with-acquaintance').trigger + require('./triggers/two-four-six-game').startGameTrigger, + require('./triggers/two-four-six-game').handleTripleGuessTrigger, + require('./triggers/two-four-six-game').handleRuleGuessTrigger, ]; const token = getToken(); diff --git a/triggers/two-four-six-game.js b/triggers/two-four-six-game.js new file mode 100644 index 0000000..622fa0d --- /dev/null +++ b/triggers/two-four-six-game.js @@ -0,0 +1,243 @@ +const { enqueueMessage } = require('../outgoing-messages'); + +// Regex to detect game start commands in Russian and English +const startGameRegex = /^[^\p{L}]*(играть|игра|сыграем|давай[^\p{L}]+игра|play|game|let'?s[^\p{L}]+play)[^\p{L}]*(2[^\p{L}]*4[^\p{L}]*6|246)?[^\p{L}]*$/ui; + +// Regex to detect number triples (three numbers separated by spaces, commas, or other non-letter characters) +const tripleRegex = /^[^\p{L}\d-]*(-?\d+(?:\.\d+)?)[^\p{L}\d-]+(-?\d+(?:\.\d+)?)[^\p{L}\d-]+(-?\d+(?:\.\d+)?)[^\p{L}\d-]*$/u; + +// Regex to detect rule guesses - phrases indicating the user is guessing the rule +const ruleGuessRegex = /^[^\p{L}]*(правило|правила|я[^\p{L}]+знаю|понял|понятно|rule|i[^\p{L}]+know|got[^\p{L}]+it|understand)[^\p{L}]*[:;]?[^\p{L}]*/ui; + +/** + * The actual rule for the 2-4-6 game: numbers must be in ascending order + */ +function checkRule(a, b, c) { + return a < b && b < c; +} + +/** + * Trigger for starting a new 2-4-6 game + */ +const startGameTrigger = { + name: "TwoFourSixGameStartTrigger", + condition: (context) => { + if (context.request.peerType !== "user") { + return false; + } + if (context?.request?.isOutbox) { + return false; + } + + // Check if user wants to start the game + const text = context.request.text || ''; + return startGameRegex.test(text); + }, + action: async (context) => { + // Initialize game state + if (!context.state) { + context.state = {}; + } + if (!context.state.games) { + context.state.games = {}; + } + + context.state.games.twoFourSixGame = { + active: true, + guesses: [], + ruleGuessesCount: 0, + lastRuleGuessDate: null, + }; + + const welcomeMessages = [ + { + ru: "Давайте сыграем в игру 2-4-6! 🎮\n\nПравила:\n• Я загадал правило для троек чисел\n• Тройка 2, 4, 6 подходит под это правило\n• Предлагайте свои тройки чисел, я скажу, подходят ли они\n• Когда будете уверены, что знаете правило — напишите его\n• У вас есть одна попытка угадать правило в день\n\nПредложите первую тройку чисел!", + en: "Let's play the 2-4-6 game! 🎮\n\nRules:\n• I have a secret rule for number triples\n• The triple 2, 4, 6 follows this rule\n• Suggest your triples, I'll tell you if they fit\n• When you're confident you know the rule — write it\n• You have one rule guess per day\n\nSuggest your first triple!" + } + ]; + + // Use Russian for this bot + const message = welcomeMessages[0].ru; + + enqueueMessage({ + ...context, + response: { + message: message + } + }); + } +}; + +/** + * Trigger for handling number triple guesses during an active game + */ +const handleTripleGuessTrigger = { + name: "TwoFourSixGameTripleGuessTrigger", + condition: (context) => { + if (context.request.peerType !== "user") { + return false; + } + if (context?.request?.isOutbox) { + return false; + } + + // Check if game is active + const gameState = context?.state?.games?.twoFourSixGame; + if (!gameState || !gameState.active) { + return false; + } + + // Check if message contains a triple + const text = context.request.text || ''; + return tripleRegex.test(text); + }, + action: async (context) => { + const text = context.request.text || ''; + const match = text.match(tripleRegex); + + if (!match) { + return; + } + + const a = parseFloat(match[1]); + const b = parseFloat(match[2]); + const c = parseFloat(match[3]); + + // Check if the triple follows the rule + const followsRule = checkRule(a, b, c); + + // Store the guess + const gameState = context.state.games.twoFourSixGame; + gameState.guesses.push({ + triple: [a, b, c], + followsRule: followsRule, + timestamp: new Date() + }); + + // Generate response + let response; + if (followsRule) { + const positiveResponses = [ + `✅ Да! Тройка ${a}, ${b}, ${c} подходит под правило.`, + `✅ Верно! ${a}, ${b}, ${c} следует правилу.`, + `✅ Правильно! Эта тройка подходит.`, + ]; + response = positiveResponses[Math.floor(Math.random() * positiveResponses.length)]; + } else { + const negativeResponses = [ + `❌ Нет. Тройка ${a}, ${b}, ${c} не подходит под правило.`, + `❌ К сожалению, ${a}, ${b}, ${c} не следует правилу.`, + `❌ Неверно. Эта тройка не подходит.`, + ]; + response = negativeResponses[Math.floor(Math.random() * negativeResponses.length)]; + } + + response += '\n\nПродолжайте предлагать тройки или попробуйте угадать правило!'; + + enqueueMessage({ + ...context, + response: { + message: response + } + }); + } +}; + +/** + * Trigger for handling rule guesses + */ +const handleRuleGuessTrigger = { + name: "TwoFourSixGameRuleGuessTrigger", + condition: (context) => { + if (context.request.peerType !== "user") { + return false; + } + if (context?.request?.isOutbox) { + return false; + } + + // Check if game is active + const gameState = context?.state?.games?.twoFourSixGame; + if (!gameState || !gameState.active) { + return false; + } + + // Check if message looks like a rule guess (not just a triple) + const text = context.request.text || ''; + + // If it's a triple, let the triple handler deal with it + if (tripleRegex.test(text)) { + return false; + } + + // Check if it contains rule-related keywords or is a longer message (likely explaining a rule) + return ruleGuessRegex.test(text) || text.length > 30; + }, + action: async (context) => { + const gameState = context.state.games.twoFourSixGame; + const now = new Date(); + + // Check if user already guessed today + if (gameState.lastRuleGuessDate) { + const lastGuessDate = new Date(gameState.lastRuleGuessDate); + const hoursSinceLastGuess = (now - lastGuessDate) / (1000 * 60 * 60); + + if (hoursSinceLastGuess < 24) { + const hoursRemaining = Math.ceil(24 - hoursSinceLastGuess); + enqueueMessage({ + ...context, + response: { + message: `⏰ Вы уже пытались угадать правило сегодня!\n\nСледующая попытка будет доступна через ${hoursRemaining} ч.\n\nА пока продолжайте проверять тройки чисел, чтобы лучше понять закономерность! 🔍` + } + }); + return; + } + } + + // User is allowed to guess + gameState.ruleGuessesCount++; + gameState.lastRuleGuessDate = now; + + const text = context.request.text || ''; + const textLower = text.toLowerCase(); + + // Check if the guess is correct (look for keywords related to ascending order) + const correctKeywords = [ + 'возраст', 'больше', 'увеличени', 'порядк', 'рост', + 'ascend', 'increas', 'order', 'grow', 'greater', 'bigger', 'larger', + 'меньше', 'средн', 'больш', // smaller, middle, bigger + 'a < b', 'b < c', 'a textLower.includes(keyword)); + + if (hasCorrectKeyword) { + // Correct guess! + gameState.active = false; + enqueueMessage({ + ...context, + response: { + message: `🎉 Поздравляю! Вы угадали правило!\n\n✨ Правило: числа должны идти в возрастающем порядке (a < b < c)\n\nЭто означает, что:\n• 2, 4, 6 подходит ✅\n• 8, 10, 12 подходит ✅\n• -1, 121, 130.5 подходит ✅\n• 2, 2, 3 НЕ подходит ❌\n• 3, 2, 1 НЕ подходит ❌\n\nЭта игра учит важному уроку: чтобы по-настоящему понять правило, нужно проверять не только примеры, которые ему следуют, но и те, которые могут его нарушить! 🧠\n\nХотите сыграть еще раз? Просто напишите "играть 246"!` + } + }); + } else { + // Incorrect guess + enqueueMessage({ + ...context, + response: { + message: `❌ К сожалению, это не то правило, которое я загадал.\n\nВы использовали свою попытку на сегодня. Следующую попытку угадать правило можно будет сделать завтра.\n\nНо вы можете продолжать проверять тройки чисел, чтобы лучше понять закономерность! Попробуйте проверить тройки, которые НЕ следуют очевидным паттернам. 🤔` + } + }); + } + } +}; + +module.exports = { + startGameTrigger, + handleTripleGuessTrigger, + handleRuleGuessTrigger, + startGameRegex, + tripleRegex, + ruleGuessRegex, + checkRule, +}; From 9e0b3967dde57c7ba0637e41bd503ed8fbfb74bc Mon Sep 17 00:00:00 2001 From: konard Date: Fri, 17 Oct 2025 20:22:58 +0300 Subject: [PATCH 3/3] Revert "Initial commit with task details for issue #26" This reverts commit a13780d3bad2bda0b69f02cb884f334a4067b080. --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 6af1f36..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: undefined -Your prepared branch: issue-26-f4950b82 -Your prepared working directory: /tmp/gh-issue-solver-1760721360375 - -Proceed. \ No newline at end of file