From 0e5214ee287b4106d79458953bb7993b3a37c504 Mon Sep 17 00:00:00 2001 From: Dragon Fire Date: Fri, 22 Jan 2021 20:37:15 -0500 Subject: [PATCH] Nim AI --- README.md | 4 +- commands/games-mp/nim.js | 202 ++++++++++++++++++++++++--------------- package.json | 2 +- 3 files changed, 129 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 027e04a6..7d0efc52 100644 --- a/README.md +++ b/README.md @@ -622,7 +622,7 @@ Total: 584 * **island:** Who will be the final two left on the island after a series of vote-kicks? * **jenga:** Play a game of Jenga with another user or the AI. * **lie-swatter:** Players are given a fact and must quickly decide if it's True or a Lie. -* **nim:** Play a game of nim with another user. +* **nim:** Play a game of nim with another user or the AI. * **pick-a-number:** Two players pick a number between 1 and 10. Whoever's closer wins. * **poker:** Play poker with up to 5 other users. * **quiz-duel:** Answer a series of quiz questions against other opponents. @@ -1620,6 +1620,8 @@ here. * psycho-pass (Original Anime) - [Psycho-Pass Wiki](https://psychopass.fandom.com/wiki/Psycho-Pass_Wiki) * psycho-pass ([Crime Coefficient Levels Data](https://psychopass.fandom.com/wiki/Crime_Coefficient_(Index%29)) +- [PuKoren](https://github.com/PuKoren) + * nim ([AI Code](https://github.com/PuKoren/ai-nim/blob/master/main.cpp)) - [r/IsTodayFridayThe13th](https://www.reddit.com/r/IsTodayFridayThe13th/) * friday-the-13th (Concept) - [Random-d.uk](https://random-d.uk/) diff --git a/commands/games-mp/nim.js b/commands/games-mp/nim.js index 6f955f42..356f3d4a 100644 --- a/commands/games-mp/nim.js +++ b/commands/games-mp/nim.js @@ -10,12 +10,21 @@ module.exports = class NimCommand extends Command { name: 'nim', group: 'games-mp', memberName: 'nim', - description: 'Play a game of nim with another user.', + description: 'Play a game of nim with another user or the AI.', + credit: [ + { + name: 'PuKoren', + url: 'https://github.com/PuKoren', + reason: 'AI Code', + reasonURL: 'https://github.com/PuKoren/ai-nim/blob/master/main.cpp' + } + ], args: [ { key: 'opponent', prompt: 'What user would you like to challenge?', - type: 'user' + type: 'user', + default: () => this.client.user }, { key: 'rows', @@ -31,16 +40,17 @@ module.exports = class NimCommand extends Command { async run(msg, { opponent, rows }) { if (opponent.id === msg.author.id) return msg.reply('You may not play against yourself.'); - if (opponent.bot) return msg.reply('Bots may not be played against.'); const current = this.client.games.get(msg.channel.id); if (current) return msg.reply(`Please wait until the current game of \`${current.name}\` is finished.`); this.client.games.set(msg.channel.id, { name: this.name }); try { - await msg.say(`${opponent}, do you accept this challenge?`); - const verification = await verify(msg.channel, opponent); - if (!verification) { - this.client.games.delete(msg.channel.id); - return msg.say('Looks like they declined...'); + if (!opponent.bot) { + await msg.say(`${opponent}, do you accept this challenge?`); + const verification = await verify(msg.channel, opponent); + if (!verification) { + this.client.games.delete(msg.channel.id); + return msg.say('Looks like they declined...'); + } } const board = this.generateBoard(rows); let userTurn = true; @@ -50,81 +60,86 @@ module.exports = class NimCommand extends Command { const objectEmoji = emoji[Math.floor(Math.random() * emoji.length)]; while (!winner) { const user = userTurn ? msg.author : opponent; - await msg.say(stripIndents` - ${user}, from which row do you want to remove from? Type \`end\` to forefeit. - After this step, you will decide how many ${objectEmoji} to remove from that row. + if (!userTurn && opponent.bot) { + const turn = this.computerTurn(board); + await msg.say(`For my turn, I remove **${turn[1]}** ${objectEmoji} from **row ${turn[0]}**.`); + } else { + await msg.say(stripIndents` + ${user}, from which row do you want to remove from? Type \`end\` to forefeit. + After this step, you will decide how many ${objectEmoji} to remove from that row. - ${this.displayBoard(board, objectEmoji)} + ${this.displayBoard(board, objectEmoji)} - ${firstTurn ? '_In Nim, you win by forcing the opponent to take the last object._' : ''} - `); - const pickFilter = res => { - if (res.author.id !== user.id) return false; - const choice = res.content; - if (choice.toLowerCase() === 'end') return true; - const i = Number.parseInt(choice, 10) - 1; - return board[i] && board[i] > 0; - }; - const turn = await msg.channel.awaitMessages(pickFilter, { - max: 1, - time: 60000 - }); - if (!turn.size) { - if (lastTurnTimeout) { - await msg.say('Game ended due to inactivity.'); - winner = 'time'; - break; - } else { - await msg.say('Sorry, time is up!'); - lastTurnTimeout = true; - userTurn = !userTurn; - continue; + ${firstTurn ? '_In Nim, you win by forcing the opponent to take the last object._' : ''} + `); + const pickFilter = res => { + if (res.author.id !== user.id) return false; + const choice = res.content; + if (choice.toLowerCase() === 'end') return true; + const i = Number.parseInt(choice, 10) - 1; + return board[i] && board[i] > 0; + }; + const turn = await msg.channel.awaitMessages(pickFilter, { + max: 1, + time: 60000 + }); + if (!turn.size) { + if (lastTurnTimeout) { + await msg.say('Game ended due to inactivity.'); + winner = 'time'; + break; + } else { + await msg.say('Sorry, time is up!'); + lastTurnTimeout = true; + userTurn = !userTurn; + continue; + } } - } - const choice = turn.first().content; - const picked = Number.parseInt(choice, 10); - if (choice.toLowerCase() === 'end') { - winner = userTurn ? opponent : msg.author; - break; - } - const row = board[picked - 1]; - await msg.say(stripIndents` - ${user}, how many ${objectEmoji} do you want to remove from row ${picked}? Type \`end\` to forefeit. - If you want to go back, type \`back\`. - - ${nums[picked - 1]}${objectEmoji.repeat(row)} - `); - const rowFilter = res => { - if (res.author.id !== user.id) return false; - const chosen = res.content; - if (chosen.toLowerCase() === 'end' || chosen.toLowerCase() === 'back') return true; - const i = Number.parseInt(chosen, 10); - return i <= row && i > 0; - }; - const rowTurn = await msg.channel.awaitMessages(rowFilter, { - max: 1, - time: 60000 - }); - if (!rowTurn.size) { - if (lastTurnTimeout) { - await msg.say('Game ended due to inactivity.'); - winner = 'time'; + const choice = turn.first().content; + const picked = Number.parseInt(choice, 10); + if (choice.toLowerCase() === 'end') { + winner = userTurn ? opponent : msg.author; break; - } else { - await msg.say('Sorry, time is up!'); - lastTurnTimeout = true; - userTurn = !userTurn; - continue; } + const row = board[picked - 1]; + await msg.say(stripIndents` + ${user}, how many ${objectEmoji} do you want to remove from row ${picked}? Type \`end\` to forefeit. + If you want to go back, type \`back\`. + + ${nums[picked - 1]}${objectEmoji.repeat(row)} + `); + const rowFilter = res => { + if (res.author.id !== user.id) return false; + const chosen = res.content; + if (chosen.toLowerCase() === 'end' || chosen.toLowerCase() === 'back') return true; + const i = Number.parseInt(chosen, 10); + return i <= row && i > 0; + }; + const rowTurn = await msg.channel.awaitMessages(rowFilter, { + max: 1, + time: 60000 + }); + if (!rowTurn.size) { + if (lastTurnTimeout) { + await msg.say('Game ended due to inactivity.'); + winner = 'time'; + break; + } else { + await msg.say('Sorry, time is up!'); + lastTurnTimeout = true; + userTurn = !userTurn; + continue; + } + } + const rowChoice = rowTurn.first().content; + const rowPicked = Number.parseInt(rowChoice, 10); + if (rowChoice.toLowerCase() === 'end') { + winner = userTurn ? opponent : msg.author; + break; + } + if (rowChoice.toLowerCase() === 'back') continue; + board[picked - 1] -= rowPicked; } - const rowChoice = rowTurn.first().content; - const rowPicked = Number.parseInt(rowChoice, 10); - if (rowChoice.toLowerCase() === 'end') { - winner = userTurn ? opponent : msg.author; - break; - } - if (rowChoice.toLowerCase() === 'back') continue; - board[picked - 1] -= rowPicked; if (!userTurn && firstTurn) firstTurn = false; if (!board.some(r => r !== 0)) { winner = userTurn ? opponent : msg.author; @@ -152,4 +167,37 @@ module.exports = class NimCommand extends Command { } return board; } + + xOr(board) { + let value = 0; + for (let i = 0; i < board.length; i++) { + value ^= board[i]; + } + return value; + } + + sum(board) { + let value = 0; + for (let i = 0; i < board.length; i++) { + value += board[i]; + } + return value; + } + + computerTurn(board) { + for (let i = 0; i < board.length; i++) { + if (board[i] > 0) { + for (let j = 1; j <= board[i]; j++) { + board[i] -= j; + const sum = this.xOr(board); + if (sum !== 0) { + board[i] += j; + } else { + return [i, j]; + } + } + } + } + return null; + } }; diff --git a/package.json b/package.json index c191fdc9..c9f9000c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xiao", - "version": "126.11.0", + "version": "126.11.1", "description": "Your personal server companion.", "main": "Xiao.js", "scripts": {