goat
command
ttt
Tic Tac Toe Pro
0
views
0
likes
0
installs
Aliases: tic, tttpro
raw source
const mongoose = require("mongoose");
const { createCanvas, loadImage } = require("canvas");
const fs = require("fs-extra");
const path = require("path");
const axios = require("axios");
const BankUser = mongoose.models.DiabloBankUser || mongoose.model("DiabloBankUser", new mongoose.Schema({
userID: { type: String, required: true, unique: true },
balance: { type: Number, default: 1000 }
}));
const TTTStats = mongoose.models.TTTStats || mongoose.model("TTTStats", new mongoose.Schema({
userID: { type: String, required: true, unique: true },
wins: { type: Number, default: 0 },
losses: { type: Number, default: 0 },
draws: { type: Number, default: 0 },
streak: { type: Number, default: 0 },
bestStreak: { type: Number, default: 0 },
xp: { type: Number, default: 0 },
rating: { type: Number, default: 0.0 }
}));
const TTTHistory = mongoose.models.TTTHistory || mongoose.model("TTTHistory", new mongoose.Schema({
threadID: String,
playerX: String,
playerO: String,
winner: { type: String, default: "draw" },
bet: Number,
time: { type: Date, default: Date.now }
}));
let games = {};
function checkWin(board, player) {
const wins = [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]];
for(let combo of wins) {
if(combo.every(i => board[i] === player)) return combo;
}
return false;
}
function updateRating(rating1, rating2, result) {
const K = 0.4;
const expected1 = 1 / (1 + Math.pow(10, (rating2 - rating1) / 4));
let newRating = rating1 + K * (result - expected1);
newRating = Math.max(0.0, Math.min(10.0, newRating));
return parseFloat(newRating.toFixed(1));
}
function formatMoney(num) {
if (num >= 1000000000000) return (num / 1000000000000).toFixed(1).replace(/\.0$/, "") + "T";
if (num >= 1000000) return (num / 1000000).toFixed(1).replace(/\.0$/, "") + "B";
if (num >= 1000000) return (num / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
if (num >= 1000) return (num / 1000).toFixed(1).replace(/\.0$/, "") + "K";
return num.toLocaleString();
}
function getLevel(xp) {
xp = xp || 0;
return Math.floor(xp / 100) + 1;
}
function parseBet(str, balance) {
if (!str) return null;
str = str.toLowerCase().trim();
if (str === "all") return 20000000;
const match = str.match(/^(\d+(\.\d+)?)\s*()?$/);
if (!match) return null;
let value = parseFloat(match[1]);
const unit = match[3];
if (unit === "k") value *= 1000;
else if (unit === "m") value *= 1000000;
else if (unit === "b") value *= 1000000;
else if (unit === "t") value *= 1000000000000;
return Math.floor(value);
}
async function getAvatar(userID) {
const url = `https://graph.facebook.com/${String(userID)}/picture?width=512&height=512`;
const res = await axios.get(url, { responseType: 'arraybuffer' });
return await loadImage(res.data);
}
async function drawProfile(stats, name, rank, avatar) {
const canvas = createCanvas(1000, 650);
const ctx = canvas.getContext("2d");
let gradient = ctx.createLinearGradient(0, 0, 1000, 650);
gradient.addColorStop(0, "#0f0f1a");
gradient.addColorStop(1, "#1e1e3f");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 1000, 650);
ctx.fillStyle = "#00ff88";
ctx.font = "bold 40px Arial";
ctx.fillText("๐ฎ ๐ง๐ง ๐ฃ๐ฅ๐ข ๐ฃ๐ฅ๐ข๐๐๐๐", 30, 60);
ctx.save();
ctx.beginPath();
ctx.arc(150, 180, 80, 0, Math.PI * 2);
ctx.closePath();
ctx.clip();
ctx.drawImage(avatar, 70, 100, 160, 160);
ctx.restore();
ctx.strokeStyle = "#00ff88";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(150, 180, 80, 0, Math.PI * 2);
ctx.stroke();
ctx.fillStyle = "#ffffff";
ctx.font = "bold 32px Arial";
ctx.fillText(name, 260, 170);
let wins = stats.wins || 0;
let losses = stats.losses || 0;
let draws = stats.draws || 0;
let xp = stats.xp || 0;
let rating = stats.rating || 0.0;
let total = wins + losses + draws;
let wr = total > 0? ((wins/total)*100).toFixed(1) : 0;
let lv = getLevel(xp);
let nextXp = lv * 100;
ctx.font = "bold 24px Arial";
ctx.fillStyle = "#ffd700";
ctx.fillText(`๐ Rank: #${rank}`, 260, 210);
ctx.fillText(`๐ Level: ${lv} | XP: ${xp}/${nextXp}`, 260, 245);
let y = 300;
ctx.fillStyle = "#ffffff";
ctx.font = "bold 22px Arial";
ctx.fillText(`๐ฏ Matches: ${total}`, 40, y);
ctx.fillText(`โ
Wins: ${wins} | โ Losses: ${losses} | ๐ค Draws: ${draws}`, 40, y+45);
ctx.fillText(`๐ฅ Streak: ${stats.streak || 0} | ๐ Best: ${stats.bestStreak || 0}`, 40, y+90);
ctx.fillText(`โญ Rating: ${rating}/10 | Winrate: ${wr}%`, 40, y+135);
ctx.fillStyle = "#00ff88";
ctx.font = "bold 20px Arial";
ctx.fillText("โโโโโโโโโโโโ", 30, 580);
ctx.fillText("Use #ttt to play and earn XP", 30, 610);
const cacheDir = path.join(__dirname, "cache");
await fs.ensureDir(cacheDir);
const cachePath = path.join(cacheDir, `tttprofile_${Date.now()}.png`);
await fs.writeFile(cachePath, canvas.toBuffer("image/png"));
return cachePath;
}
async function drawBoardImage(board, p1Name, p2Name, bet, winCombo = null, result = null) {
const canvas = createCanvas(900, 580);
const ctx = canvas.getContext("2d");
let gradient = ctx.createLinearGradient(0, 0, 900, 580);
gradient.addColorStop(0, "#0a0a0f");
gradient.addColorStop(1, "#1a1a2e");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 900, 580);
ctx.fillStyle = "#00ff88";
ctx.font = "bold 36px Arial";
ctx.fillText("๐ฎ ๐ง๐๐ ๐ง๐๐ ๐ง๐ข๐ ๐ฃ๐ฅ๐ข", 30, 55);
ctx.font = "bold 20px Arial";
ctx.fillStyle = "#ffd700";
ctx.fillText(`๐ฐ Bet: $${formatMoney(bet)}`, 30, 90);
ctx.fillStyle = "#ffffff";
ctx.font = "bold 22px Arial";
ctx.fillText(`โ: ${p1Name}`, 30, 130);
ctx.fillText(`โญ: ${p2Name}`, 30, 160);
const startX = 300, startY = 210, size = 120;
ctx.strokeStyle = "#00ff88";
ctx.lineWidth = 8;
for(let i = 1; i < 3; i++) {
ctx.beginPath();
ctx.moveTo(startX + i * size, startY);
ctx.lineTo(startX + i * size, startY + 3 * size);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(startX, startY + i * size);
ctx.lineTo(startX + 3 * size, startY + i * size);
ctx.stroke();
}
for(let i = 0; i < 9; i++) {
let x = startX + (i % 3) * size + size/2;
let y = startY + Math.floor(i / 3) * size + size/2;
ctx.font = "bold 75px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
if(board[i] == "X") {
ctx.fillStyle = "#ff3366";
ctx.fillText("โ", x, y);
} else if(board[i] == "O") {
ctx.fillStyle = "#3399ff";
ctx.fillText("โญ", x, y);
}
}
if(winCombo) {
ctx.strokeStyle = "#ff0000";
ctx.lineWidth = 12;
ctx.lineCap = "round";
const [a,b,c] = winCombo;
const ax = startX + (a % 3) * size + size/2, ay = startY + Math.floor(a / 3) * size + size/2;
const cx = startX + (c % 3) * size + size/2, cy = startY + Math.floor(c / 3) * size + size/2;
ctx.beginPath();
ctx.moveTo(ax, ay);
ctx.lineTo(cx, cy);
ctx.stroke();
}
if(result == 'draw') {
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(0, 0, 900, 580);
ctx.fillStyle = "#ffcc00";
ctx.font = "bold 100px Arial";
ctx.textAlign = "center";
ctx.fillText("DRAW", 450, 320);
}
ctx.textAlign = "left";
const cacheDir = path.join(__dirname, "cache");
await fs.ensureDir(cacheDir);
const cachePath = path.join(cacheDir, `ttt_${Date.now()}.png`);
await fs.writeFile(cachePath, canvas.toBuffer("image/png"));
return cachePath;
}
async function setGameTimer(tid, game, usersData, api) {
if(game.timer) clearTimeout(game.timer);
game.timer = setTimeout(async () => {
if(!games[tid]) return;
let g = games[tid];
let winnerID = g.turn == g.playerX? g.playerO : g.playerX;
let loserID = g.turn;
let winner = await BankUser.findOne({ userID: winnerID });
let loser = await BankUser.findOne({ userID: loserID });
winner.balance += g.bet * 2;
loser.balance -= g.bet;
let wStats = await TTTStats.findOne({ userID: winnerID }) || await TTTStats.create({ userID: winnerID });
let lStats = await TTTStats.findOne({ userID: loserID }) || await TTTStats.create({ userID: loserID });
let oldWR = wStats.rating || 0.0; let oldLR = lStats.rating || 0.0;
wStats.rating = updateRating(oldWR, oldLR, 1);
lStats.rating = updateRating(oldLR, oldWR, 0);
wStats.wins = (wStats.wins || 0) + 1; wStats.streak = (wStats.streak || 0) + 1; wStats.xp = (wStats.xp || 0) + 50;
lStats.losses = (lStats.losses || 0) + 1; lStats.streak = 0; lStats.xp = (lStats.xp || 0) + 10;
if(wStats.streak > (wStats.bestStreak || 0)) wStats.bestStreak = wStats.streak;
await winner.save(); await loser.save(); await wStats.save(); await lStats.save();
await TTTHistory.create({threadID: tid, playerX: g.playerX, playerO: g.playerO, winner: winnerID, bet: g.bet});
let loserName = await usersData.getName(String(loserID));
delete games[tid];
api.sendMessage(`โฐ ๐ง๐๐ ๐ ๐จ๐ฃ!\n${loserName} 5min e move dei nai\n๐ ๐ช๐๐ก๐๐ฅ ๐๐ฌ ๐๐๐๐๐จ๐๐ง: ${await usersData.getName(String(winnerID))}\n๐ฐ ๐ช๐ข๐ก: $${formatMoney(g.bet*2)}`, tid);
}, 5 * 60 * 1000);
}
module.exports = {
config: {
name: "ttt",
aliases: ["tic", "tttpro"],
version: "5.4.0",
author: "Sazzad",
countDown: 3,
role: 0,
shortDescription: "Tic Tac Toe Pro",
category: "game",
guide: { en: "{p}ttt @tag 500b/all\n{p}ttt 1-9\n{p}ttt top\n{p}ttt p/@tag\n{p}ttt h - history" }
},
onStart: async function({ api, event, args, message, usersData }) {
const tid = event.threadID;
const MIN_BET = 1;
const MAX_BET = 20000000;
await TTTStats.updateMany({rating: {$gt: 10}}, { $set: { rating: 0.0 } });
const getNameSafe = async (uid) => {
if (!uid) return "Unknown";
return await usersData.getName(String(uid));
}
if(args[0] == "h" || args[0] == "history") {
let history = await TTTHistory.find({threadID: tid}).sort({time: -1}).limit(10);
if(history.length == 0) return message.reply("๐ Ekhono kono match hoi nai");
let msg = `๐ ๐๐๐ฆ๐ง 10 ๐ ๐๐ง๐๐๐๐ฆ\nโโโโโโโโโโโโ\n`;
for(let i = 0; i < history.length; i++) {
let p1 = await getNameSafe(history[i].playerX);
let p2 = await getNameSafe(history[i].playerO);
let result = history[i].winner == "draw"? "๐ค Draw" : `๐ Win: ${await getNameSafe(history[i].winner)}`;
msg += `\n${i+1}. ${p1} vs ${p2}\n๐ฐ $${formatMoney(history[i].bet)} | ${result}`;
}
return message.reply(msg);
}
if(args[0] == "p" || args[0] == "profile") {
let uid = event.senderID;
if(event.mentions && Object.keys(event.mentions).length > 0) uid = Object.keys(event.mentions)[0];
let stats = await TTTStats.findOne({ userID: uid }) || await TTTStats.create({ userID: uid });
let name = await getNameSafe(uid);
let avatar = await getAvatar(uid);
let all = await TTTStats.find().sort({rating: -1});
let rank = all.findIndex(u => u.userID == uid) + 1;
let img = await drawProfile(stats, name, rank, avatar);
return message.reply({attachment: fs.createReadStream(img)});
}
if(args[0] == "top") {
let top = await TTTStats.find().sort({rating: -1}).limit(10);
let msg = `๐ ๐ง๐ง ๐ฃ๐ฅ๐ข ๐๐๐๐๐๐ฅ๐๐ข๐๐ฅ๐ ๐\nโโโโโโโโโโโโโ\n`;
for(let i = 0; i < top.length; i++) {
let name = await getNameSafe(top[i].userID);
let wins = top[i].wins || 0;
let losses = top[i].losses || 0;
let draws = top[i].draws || 0;
let xp = top[i].xp || 0;
let total = wins + losses + draws;
let wr = total > 0? ((wins/total)*100).toFixed(1) : 0;
let lv = getLevel(xp);
let rating = (top[i].rating || 0.0).toFixed(1);
msg += `\n๐
#${i+1} ${name} [Lv.${lv}] | โญ ${rating}/10`;
msg += `\n๐ฏ Matches: ${total} | โ
Wins: ${wins} | โ Losses: ${losses} | ๐ค Draws: ${draws}`;
msg += `\n๐ WR: ${wr}% | ๐ฅ Streak: ${top[i].streak || 0} | ๐ Best: ${top[i].bestStreak || 0}\n`;
}
return message.reply(msg);
}
let g = games[tid];
let isMove = args[0] &&!isNaN(args[0]) && g;
if (isMove) {
let pos = parseInt(args[0]) - 1;
if (pos < 0 || pos > 8) return message.reply("โ 1-9 er moddhe dao");
if (g.board[pos] == "X" || g.board[pos] == "O") return message.reply("โ Already taken");
if (event.senderID!= g.turn) return message.reply("โ Tomar turn na");
let mark = g.playerX == event.senderID? "X" : "O";
g.board[pos] = mark;
setGameTimer(tid, g, usersData, api);
let p1Name = await getNameSafe(g.playerX);
let p2Name = await getNameSafe(g.playerO);
let winCombo = checkWin(g.board, mark);
let img = await drawBoardImage(g.board, p1Name, p2Name, g.bet, winCombo);
if (winCombo) {
clearTimeout(g.timer);
let winnerID = event.senderID;
let loserID = winnerID == g.playerX? g.playerO : g.playerX;
let winner = await BankUser.findOne({ userID: winnerID });
let loser = await BankUser.findOne({ userID: loserID });
winner.balance += g.bet;
loser.balance -= g.bet;
let wStats = await TTTStats.findOne({ userID: winnerID }) || await TTTStats.create({ userID: winnerID });
let lStats = await TTTStats.findOne({ userID: loserID }) || await TTTStats.create({ userID: loserID });
let oldWR = wStats.rating || 0.0;
let oldLR = lStats.rating || 0.0;
wStats.rating = updateRating(oldWR, oldLR, 1);
lStats.rating = updateRating(oldLR, oldWR, 0);
wStats.wins = (wStats.wins || 0) + 1;
wStats.streak = (wStats.streak || 0) + 1;
wStats.xp = (wStats.xp || 0) + 50;
if(wStats.streak > (wStats.bestStreak || 0)) wStats.bestStreak = wStats.streak;
lStats.losses = (lStats.losses || 0) + 1;
lStats.streak = 0;
lStats.xp = (lStats.xp || 0) + 10;
await winner.save();
await loser.save();
await wStats.save();
await lStats.save();
await TTTHistory.create({threadID: tid, playerX: g.playerX, playerO: g.playerO, winner: winnerID, bet: g.bet});
let total = wStats.wins + wStats.losses + (wStats.draws || 0);
let wr = total > 0? ((wStats.wins/total)*100).toFixed(1) : 0;
delete games[tid];
let finalImg = await drawBoardImage(g.board, p1Name, p2Name, g.bet, winCombo, 'win');
return message.reply({ body: `๐ ๐ช๐๐ก๐๐ฅ: ${await getNameSafe(winnerID)}\n๐ฐ ๐ช๐ข๐ก: $${formatMoney(g.bet*2)}\nโญ Rating: ${wStats.rating}/10 | Winrate: ${wr}% | +50 XP\nโโโโโโโโโโโ\nโ ${p1Name}\nโญ ${p2Name}\n๐ต Bet: $${formatMoney(g.bet)}`, attachment: fs.createReadStream(finalImg) });
}
if (!g.board.find(i =>!isNaN(i))) {
clearTimeout(g.timer);
let p1Stats = await TTTStats.findOne({ userID: g.playerX }) || await TTTStats.create({ userID: g.playerX });
let p2Stats = await TTTStats.findOne({ userID: g.playerO }) || await TTTStats.create({ userID: g.playerO });
let oldP1R = p1Stats.rating || 0.0;
let oldP2R = p2Stats.rating || 0.0;
p1Stats.rating = updateRating(oldP1R, oldP2R, 0.5);
p2Stats.rating = updateRating(oldP2R, oldP1R, 0.5);
p1Stats.draws = (p1Stats.draws || 0) + 1;
p1Stats.streak = 0;
p1Stats.xp = (p1Stats.xp || 0) + 20;
p2Stats.draws = (p2Stats.draws || 0) + 1;
p2Stats.streak = 0;
p2Stats.xp = (p2Stats.xp || 0) + 20;
await p1Stats.save();
await p2Stats.save();
await TTTHistory.create({threadID: tid, playerX: g.playerX, playerO: g.playerO, winner: "draw", bet: g.bet});
delete games[tid];
let finalImg = await drawBoardImage(g.board, p1Name, p2Name, g.bet, null, 'draw');
return message.reply({ body: `๐ค ๐๐ฅ๐๐ช!\n๐ฐ ๐๐ฒ๐ ๐ฅ๐ฒ๐ณ๐๐ป๐ฑ๐ฒ๐ฑ | โญ Rating: ${p1Stats.rating}/10 / ${p2Stats.rating}/10 | +20 XP\nโโโโโโโโโโโ\nโ ${p1Name}\nโญ ${p2Name}\n๐ต Bet: $${formatMoney(g.bet)}`, attachment: fs.createReadStream(finalImg) });
}
g.turn = g.turn == g.playerX? g.playerO : g.playerX;
let nextName = await getNameSafe(g.turn);
return message.reply({body: `โญ ๐ง๐จ๐ฅ๐ก: ${nextName}\n๐ฐ Bet: $${formatMoney(g.bet)} | โฐ 5min time`, attachment: fs.createReadStream(img)});
}
let p1 = event.senderID;
let bet = parseBet(args[args.length-1], 0);
if (!bet) return message.reply("โ Bet amount dao\nEx: #ttt @tag 500b or #ttt @tag all");
if (bet < MIN_BET) return message.reply(`โ Min bet $1`);
if (bet > MAX_BET) return message.reply(`โ Max bet 20T`);
let p2 = null;
if (event.type == "message_reply") p2 = event.messageReply.senderID;
else if (event.mentions && Object.keys(event.mentions).length > 0) p2 = Object.keys(event.mentions)[0];
if (!p2) return message.reply("โ Use: #ttt @tag 500b");
if (p1 == p2) return message.reply("โ Nijer sathe khela jabe na");
let user1 = await BankUser.findOne({ userID: p1 }) || await BankUser.create({ userID: p1, balance: 1000 });
let user2 = await BankUser.findOne({ userID: p2 }) || await BankUser.create({ userID: p2, balance: 1000 });
if (user1.balance < bet) return message.reply(`โ Tomar balance kom`);
if (user2.balance < bet) return message.reply(`โ Opponent er balance kom`);
let name1 = await getNameSafe(p1);
let name2 = await getNameSafe(p2);
games[tid] = { board: ["1","2","3","4","5","6","7","8","9"], playerX: p1, playerO: p2, turn: p1, bet: bet };
setGameTimer(tid, games[tid], usersData, api);
let img = await drawBoardImage(games[tid].board, name1, name2, bet);
return message.reply({ body: `๐ฎ ๐๐๐ ๐ ๐ฆ๐ง๐๐ฅ๐ง - Bet: $${formatMoney(bet)}\nโ = ${name1} $${formatMoney(user1.balance)}\nโญ = ${name2} $${formatMoney(user2.balance)}\n๐ฏ First turn: ${name1}\nโฐ 5min er moddhe move dite hobe`, attachment: fs.createReadStream(img) });
}
};