← back to browse
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) });
  }
};

View raw file