[OpenSource] TwentyOne AssistSidebar
♠ Twenty OneDescription
About This Script Twenty-One Assist is a sidebar helper for Roblox Twenty-One games (the 1–11 card format with trump cards and hidden "Husk" cards). It doesn't play for you. It tracks every card that's been dealt, works out what's left, and tells you where you stand so you can make better calls each round. Author: @Skidder As of October 5, 2026, this runs correctly on Potassium for PC. Functionality on mobile devices is currently unconfirmed (feel free to test it yourself). If you encounter issues, please note that—in addition to providing feedback—this script is open-source. You are free to modify and optimize it, provided you retain the original author's information. How it works The script listens to the game's own events (cards being added, trumps played, bet changes, target score updates, table clears and reveals) and builds a picture of the round from them. It recognizes new rounds automatically and keeps a ledger of cards that have already shown up, even after they leave the table. That means the remaining pool stays accurate when cards get returned or removed mid-round. Main features - Deck tracker: All 11 cards sit in a single row, color-coded by status: in your hand (green), opponent's visible cards (red), assigned to the opponent's hidden card (gold), or marked as still in the deck (blue). You can use the ▲▼ arrows on each card to manually set it to Auto, Husk or Deck if you know something the script doesn't. - Hidden card deduction: When you play an N-Card or Perfect Draw and don't get the card, the script figures out the opponent is probably holding it and marks it automatically. - Husk analysis: For every possible value of the opponent's hidden card, it shows whether they'd win, tie or lose against your current total. If they have more than one hidden card, it lists every possible combination with its total, color-coded by result. - Opponent range: Shows the lowest and highest score the opponent could have, along with their visible total, number of hidden cards and a midpoint estimate. - Advice panel: Gives quick suggestions based on your hand: your bust risk if you draw, whether a perfect draw is still possible (and why not, if it isn't), and when you should just stand. It also flags useful trumps you're holding, like a matching N-Card, Perfect Draw, Return/Remove when you've busted, or Go for 17 when the opponent is above 17. - Trump tracking: Lists your current trumps and estimates how many the opponent has from what they've gained and played. - Log: Shows recent events such as round starts, opponent trump plays, deductions and end-of-round reveals. UI The UI is a clean, black, minimal sidebar on the right side of the screen. You can resize it by dragging the left edge, and there are sliders at the top for opacity and text size. Every section can be collapsed, and the ◀ button tucks the whole sidebar into a small tab. Clicks on the panel don't go through to the game, so you won't accidentally interact with the world behind it. Controls - RightShift: show/hide the sidebar - F6: copy a raw dump of recent game events to your clipboard (useful for debugging or if the game updates) - End: fully unload the script and clean up everything it created Works best in standard "Mr.Sawn"(I've forgotten what it's called.) Twenty-One tables that use the usual event names. If the game changes its remotes, the F6 dump makes it easy to see what's different.
Script source Luau
-- [[ BloxScripts Risk Notice ]]
-- This script has not been verified by BloxScripts.fun. Deal with caution.
--
-- Stay safe:
-- • Never log in on unofficial Roblox sites or lookalike domains.
-- • Real Roblox links use roblox.com (check the .com ending).
-- • Treat fake Roblox login / "claim reward" pages as phishing.
-- [[ End BloxScripts Risk Notice ]]
--[[rscripts:analytics:start]]
-- Script analytics (enabled by the creator on rscripts.net).
-- Runs in its own thread and cannot affect the script below.
task.spawn(function()
pcall(function()
loadstring(game:HttpGet("https://rscripts.net/api/telemetry/v2/client.lua?s=6ac491863c4292a0bde8a418"))()
end)
end)
--[[rscripts:analytics:end]]
-- =====================================================================
-- TWENTY-ONE ASSIST // Sidebar V1
-- RightShift toggle | F6 copy dump | End unload
-- Drag left edge to resize | Top: opacity/font size | ◀ collapse
-- Use ▲▼ on each card to change its state
--
-- V1 UI:
-- · Black / Vercel-like visual style
-- · UI captures mouse input instead of passing it to the game world
-- · All 11 cards always remain on one row
-- · Adjustable text size
-- · Fixed right sidebar resize calculation
-- =====================================================================
local RS = game:GetService("ReplicatedStorage")
local UIS = game:GetService("UserInputService")
local Run = game:GetService("RunService")
local Plr = game:GetService("Players")
local LP = Plr.LocalPlayer
local CFG = {
width = 430,
minW = 350,
maxW = 650,
opacity = 0.50,
fontSize = 16,
minFont = 12,
maxFont = 20,
toggleKey = Enum.KeyCode.RightShift,
dumpKey = Enum.KeyCode.F6,
unloadKey = Enum.KeyCode.End,
cursor = "rbxasset://textures/Cursors/KeyboardMouse/ArrowFarCursor.png",
dumpEvents = {
"AddCards",
"PlayedTrumpVisual",
"UpdatePlayerTrumps",
"AddingTrumpVisual",
"ClearTable",
"ChangeVisualBet",
"UpdateWinningScore",
"ShowHiddenCards",
},
}
-- Vercel / Linear-style theme
local TH = {
bg = Color3.fromRGB(5, 5, 5),
surface = Color3.fromRGB(12, 12, 12),
surface2 = Color3.fromRGB(20, 20, 20),
hover = Color3.fromRGB(28, 28, 28),
border = Color3.fromRGB(42, 42, 42),
border2 = Color3.fromRGB(62, 62, 62),
accent = Color3.fromRGB(255, 255, 255),
red = Color3.fromRGB(220, 64, 64),
redD = Color3.fromRGB(92, 30, 30),
-- Darker gold improves contrast with white text.
gold = Color3.fromRGB(145, 105, 24),
goldText = Color3.fromRGB(245, 196, 72),
green = Color3.fromRGB(35, 155, 82),
greenD = Color3.fromRGB(19, 82, 47),
blue = Color3.fromRGB(45, 105, 190),
gray = Color3.fromRGB(38, 38, 38),
text = Color3.fromRGB(250, 250, 250),
text2 = Color3.fromRGB(212, 212, 212),
dim = Color3.fromRGB(140, 140, 140),
dim2 = Color3.fromRGB(92, 92, 92),
white = Color3.fromRGB(255, 255, 255),
}
-- ============================ STATE ============================
local S = {
round=0,
target=21,
bet=nil,
myHand={},
oppHand={},
myTrumps={},
oppGain=0,
oppPlays=0,
tableMe={},
tableOpp={},
deduce={},
user={},
-- ledger contains cards confirmed to have appeared this round
-- but which may no longer be present in the current snapshot.
ledger={},
pool={},
oppHusk=0,
oppVis=0,
pending=nil,
log={},
hasSnapshot=false,
lastAddAt=0,
roundEnded=false,
}
local Bus={h={}}
function Bus.on(e,f)
local t=Bus.h[e]
if not t then
t={}
Bus.h[e]=t
end
t[#t+1]=f
end
function Bus.emit(e,...)
local t=Bus.h[e]
if t then
for _,f in ipairs(t) do
pcall(f,...)
end
end
end
local function now()
return os.clock()
end
local function log(s)
S.log[#S.log+1]=os.date("[%H:%M:%S] ")..s
if #S.log>200 then
table.remove(S.log,1)
end
end
-- ============================ INFERENCE ============================
local Infer={}
function Infer.inMine(v)
for _,c in ipairs(S.myHand) do
if c.v==v then
return true
end
end
return false
end
function Infer.inOppVis(v)
for _,c in ipairs(S.oppHand) do
if c.v==v then
return true
end
end
return false
end
function Infer.mySum()
local s=0
for _,c in ipairs(S.myHand) do
s=s+(c.v or 0)
end
return s
end
-- ============================================================
-- Final score comparison
--
-- Returns:
-- 1 = A beats B
-- 0 = Tie
-- -1 = A loses to B
--
-- Rules:
-- · Not bust > bust
-- · Neither bust: higher score wins
-- · Both bust: smaller amount over the target wins
-- · Identical result: tie
-- ============================================================
function Infer.compareScore(a,b,T)
T=T or S.target
local aBust=a>T
local bBust=b>T
if aBust~=bBust then
return aBust and -1 or 1
end
if not aBust then
if a>b then return 1 end
if a<b then return -1 end
return 0
end
local ao=a-T
local bo=b-T
if ao<bo then return 1 end
if ao>bo then return -1 end
return 0
end
-- Current known player total.
-- Any locally known value in myHand is included.
function Infer.myFinalKnown()
return Infer.mySum()
end
-- ============================================================
-- CURRENT KNOWN CARDS
-- ============================================================
function Infer.currentKnownSet()
local known={}
for _,c in ipairs(S.myHand) do
if type(c.v)=="number" then
known[c.v]=true
end
end
for _,c in ipairs(S.oppHand) do
if type(c.v)=="number" then
known[c.v]=true
end
end
return known
end
-- ============================================================
-- RECOMPUTE CARD POOL
--
-- pool contains cards that:
-- · are not in the current player's hand
-- · are not among the opponent's visible cards
-- · have not already been recorded in ledger
--
-- A Husk's actual value is unknown, so its possible number
-- naturally remains in the pool.
-- ============================================================
function Infer.recompute()
local known=Infer.currentKnownSet()
local pool={}
for v=1,11 do
if not known[v] and not S.ledger[v] then
pool[#pool+1]=v
end
end
table.sort(pool)
S.pool=pool
local k,sv=0,0
for _,c in ipairs(S.oppHand) do
if c.husk then
k=k+1
else
sv=sv+(c.v or 0)
end
end
S.oppHusk=k
S.oppVis=sv
for v in pairs(S.deduce) do
if known[v] or S.ledger[v] then
S.deduce[v]=nil
end
end
end
function Infer.huskCands()
local r={}
for _,v in ipairs(S.pool) do
if not S.deduce[v]
and S.user[v]~="deck" then
r[#r+1]=v
end
end
return r
end
function Infer.deckCands()
local r={}
for _,v in ipairs(S.pool) do
if not S.deduce[v]
and S.user[v]~="husk" then
r[#r+1]=v
end
end
return r
end
-- ============================================================
-- MULTIPLE HUSK COMBINATIONS
--
-- pinned:
-- Cards already inferred or manually assigned as Husk.
--
-- For the remaining Husks, choose the required number of cards
-- from the candidate pool.
-- ============================================================
function Infer.huskCombinations()
local k=S.oppHusk
if k<=0 then
return {}
end
local pinned={}
local pinnedSet={}
for _,v in ipairs(S.pool) do
if S.deduce[v] or S.user[v]=="husk" then
if not pinnedSet[v] then
pinnedSet[v]=true
pinned[#pinned+1]=v
end
end
end
table.sort(pinned)
if #pinned>k then
return {}
end
local free={}
for _,v in ipairs(S.pool) do
if not pinnedSet[v]
and S.user[v]~="deck" then
free[#free+1]=v
end
end
table.sort(free)
local need=k-#pinned
local raw={}
if need==0 then
local c={}
for _,v in ipairs(pinned) do
c[#c+1]=v
end
raw[1]=c
elseif need>0 and #free>=need then
local picked={}
local function choose(startIndex,left)
if left==0 then
local c={}
for _,v in ipairs(pinned) do
c[#c+1]=v
end
for _,v in ipairs(picked) do
c[#c+1]=v
end
table.sort(c)
raw[#raw+1]=c
return
end
local last=#free-left+1
for i=startIndex,last do
picked[#picked+1]=free[i]
choose(i+1,left-1)
picked[#picked]=nil
end
end
choose(1,need)
end
local out={}
local my=Infer.myFinalKnown()
for _,cards in ipairs(raw) do
local sum=0
for _,v in ipairs(cards) do
sum=sum+v
end
local total=S.oppVis+sum
out[#out+1]={
cards=cards,
sum=sum,
total=total,
-- Result from the opponent's perspective:
-- 1 opponent wins
-- 0 tie
-- -1 opponent loses
result=Infer.compareScore(
total,
my,
S.target
)
}
end
return out
end
-- ============================================================
-- HUSK CARD COLOR MEANING
--
-- With one Husk, the result is exact.
--
-- With multiple Husks:
-- · all combinations containing it lose -> red
-- · all combinations containing it win -> green
-- · all combinations containing it tie -> yellow
-- · mixed outcomes -> mixed
-- ============================================================
function Infer.evalEnemyHusk(v)
if S.oppHusk<=1 then
local enemyTotal=S.oppVis+(tonumber(v) or 0)
local myTotal=Infer.myFinalKnown()
return Infer.compareScore(
enemyTotal,
myTotal,
S.target
),enemyTotal,myTotal
end
local combos=Infer.huskCombinations()
local win,tie,lose=0,0,0
for _,combo in ipairs(combos) do
if table.find(combo.cards,v) then
if combo.result>0 then
win=win+1
elseif combo.result<0 then
lose=lose+1
else
tie=tie+1
end
end
end
local n=win+tie+lose
if n==0 then
return nil,nil,Infer.myFinalKnown(),"none"
end
if win==n then
return 1,nil,Infer.myFinalKnown(),"win"
end
if lose==n then
return -1,nil,Infer.myFinalKnown(),"lose"
end
if tie==n then
return 0,nil,Infer.myFinalKnown(),"tie"
end
return nil,nil,Infer.myFinalKnown(),"mixed"
end
-- ============================================================
-- COMPLETE OPPONENT HUSK OUTCOME DISTRIBUTION
-- ============================================================
function Infer.enemyHuskOutcome()
local combos=Infer.huskCombinations()
local out={
win={},
tie={},
lose={},
combos=combos,
allLose=false,
allWin=false,
allTie=false,
count=#combos,
}
for _,combo in ipairs(combos) do
if combo.result>0 then
out.win[#out.win+1]=combo
elseif combo.result<0 then
out.lose[#out.lose+1]=combo
else
out.tie[#out.tie+1]=combo
end
end
if #combos>0 then
out.allLose=(#out.lose==#combos)
out.allWin=(#out.win==#combos)
out.allTie=(#out.tie==#combos)
end
return out
end
-- ============================================================
-- OPPONENT POSSIBLE SCORE RANGE
-- ============================================================
function Infer.oppRange()
if S.oppHusk<=0 then
return S.oppVis,S.oppVis
end
local combos=Infer.huskCombinations()
if #combos==0 then
return S.oppVis,S.oppVis
end
local mn=math.huge
local mx=-math.huge
for _,combo in ipairs(combos) do
mn=math.min(mn,combo.total)
mx=math.max(mx,combo.total)
end
return mn,mx
end
-- Returns whether a numbered card has already been confirmed.
function Infer.numberKnown(v)
if type(v)~="number" then
return false
end
if Infer.inMine(v) then
return true
end
if Infer.inOppVis(v) then
return true
end
if S.ledger[v] then
return true
end
if S.deduce[v] then
return true
end
return false
end
function Infer.perfectDrawPossible()
local my=Infer.mySum()
local need=S.target-my
if need<1 or need>11 then
return false,need,"required value is outside the card range"
end
if Infer.inOppVis(need) then
return false,need,("%d is already visible in the opponent's hand"):format(need)
end
if Infer.inMine(need) then
return false,need,("%d is already in your hand"):format(need)
end
if S.ledger[need] then
return false,need,("%d has already appeared this round"):format(need)
end
if S.deduce[need] or S.user[need]=="husk" then
return false,need,("%d has been assigned to the opponent's Husk"):format(need)
end
local cands=Infer.deckCands()
if not table.find(cands,need) then
return false,need,("%d is not a deck candidate"):format(need)
end
return true,need,nil
end
function Infer.deduceOpp(v,why)
if type(v)~="number" then
return
end
if Infer.inMine(v) then
log(("Rejected opponent hidden=%d (%s): in your hand"):format(v,why))
return
end
if Infer.inOppVis(v) then
log(("Rejected opponent hidden=%d (%s): opponent visible card"):format(v,why))
return
end
if S.ledger[v] then
log(("Rejected opponent hidden=%d (%s): already appeared this round"):format(v,why))
return
end
if S.deduce[v] then
return
end
S.deduce[v]=why
log(
("Inferred opponent hidden=%d (%s) pool={%s}")
:format(v,why,table.concat(S.pool,","))
)
end
function Infer.advise()
local L={}
local my=Infer.mySum()
local T=S.target
local cands=Infer.deckCands()
local mn,mx=Infer.oppRange()
local need=T-my
local outcome=Infer.enemyHuskOutcome()
L[#L+1]=(
"You %d / Target %d / Opponent %d~%d"
):format(my,T,mn,mx)
if S.oppHusk>0 and outcome.count>0 and outcome.allLose then
L[#L+1]="✓ All opponent Husk outcomes lose"
L[#L+1]="→ Stand / keep current score"
return L
end
if my==T then
L[#L+1]="✓ Exact target"
L[#L+1]="→ Stand"
return L
end
if my>T then
L[#L+1]=("Bust +%d"):format(my-T)
if table.find(S.myTrumps,"Return")
or table.find(S.myTrumps,"Remove") then
L[#L+1]="→ Consider Return / Remove first"
else
if S.oppHusk>0
and outcome.count>0
and outcome.allLose then
L[#L+1]="All opponent candidates bust by more than you"
L[#L+1]="→ Stand"
end
end
return L
end
local bust=0
for _,d in ipairs(cands) do
if my+d>T then
bust=bust+1
end
end
local bustPct=
#cands>0
and math.floor(bust/#cands*100)
or 0
L[#L+1]=(
"Need %d | Draw bust risk %d%%"
):format(need,bustPct)
if need>=1 and need<=11 then
local possible,perfectNeed,why=
Infer.perfectDrawPossible()
if possible then
L[#L+1]=(
"Perfect Draw = %d (possible)"
):format(perfectNeed)
if table.find(
S.myTrumps,
perfectNeed.."-Card"
) then
L[#L+1]=(
"★ %d-Card available"
):format(perfectNeed)
end
if table.find(
S.myTrumps,
"Perfect Draw"
) then
L[#L+1]="★ Perfect Draw available"
end
else
L[#L+1]=(
"Perfect Draw = %d (unavailable: %s)"
):format(
perfectNeed,
why or "unknown"
)
end
end
if S.oppHusk>0 and outcome.count>0 then
L[#L+1]=(
S.oppHusk>=2
and "Opponent Husk combinations: W%d / T%d / L%d"
or "Opponent Husk: W%d / T%d / L%d"
):format(
#outcome.win,
#outcome.tie,
#outcome.lose
)
end
if mn>T then
local cmp=Infer.compareScore(
mn,
my,
T
)
if cmp<0 then
L[#L+1]="Even the opponent's minimum score loses to yours"
L[#L+1]="→ Stand"
return L
end
end
if table.find(S.myTrumps,"Go for 17")
and mn>17 then
L[#L+1]="★ Opponent >17 → consider Go for 17"
end
if #cands==0 then
L[#L+1]="No deck candidates → Stand"
end
return L
end
-- ============================ EVENTS ============================
local Core={}
local function trumpKey(t)
return tostring(t and t[1])..":"..tostring(t and t[2])
end
local function diffAdded(prev,nowL)
local ids={}
for _,t in ipairs(prev or {}) do
ids[trumpKey(t)]=true
end
local r={}
for _,t in ipairs(nowL or {}) do
local k=trumpKey(t)
if not ids[k] then
r[#r+1]=t
end
end
return r
end
local function normTrumpList(x)
local out={}
if type(x)~="table" then
return out
end
if type(x[1])=="string" then
out[1]={x[1],x[2]}
else
for _,e in ipairs(x) do
if type(e)=="table" and type(e[1])=="string" then
out[#out+1]={e[1],e[2]}
end
end
end
return out
end
local function numericKnownFromHands(myHand,oppHand)
local known={}
for _,c in ipairs(myHand or {}) do
if type(c.v)=="number" then
known[c.v]=true
end
end
for _,c in ipairs(oppHand or {}) do
if type(c.v)=="number" then
known[c.v]=true
end
end
return known
end
local function parseHands(a1,a2)
local my={}
local opp={}
for _,c in ipairs(a1 or {}) do
my[#my+1]={
v=type(c[1])=="number" and c[1] or nil,
husk=c[2]==true
}
end
for _,c in ipairs((a2 and a2[2]) or {}) do
opp[#opp+1]={
v=type(c[1])=="number" and c[1] or nil,
husk=c[2]==true
}
end
return my,opp
end
local function hardResetRound(reason)
S.round=S.round+1
S.ledger={}
S.deduce={}
S.user={}
S.tableMe={}
S.tableOpp={}
S.pending=nil
S.oppGain=0
S.oppPlays=0
S.roundEnded=false
log(
("=== Round %d / %s ===")
:format(
S.round,
reason or "reset"
)
)
end
function Core.onAddCards(a1,a2)
local newMy,newOpp=parseHands(a1,a2)
local oldMy=S.myHand
local oldOpp=S.oppHand
local oldKnown=
numericKnownFromHands(
oldMy,
oldOpp
)
local newKnown=
numericKnownFromHands(
newMy,
newOpp
)
local oldCount=#oldMy+#oldOpp
local newCount=#newMy+#newOpp
local replaced=0
local overlap=0
for v in pairs(oldKnown) do
if newKnown[v] then
overlap=overlap+1
else
replaced=replaced+1
end
end
local looksFresh=false
if S.roundEnded then
looksFresh=true
elseif S.hasSnapshot and oldCount>0 and newCount>0 then
if newCount<oldCount then
looksFresh=true
elseif oldCount>=5
and newCount<=4
and replaced>=2 then
looksFresh=true
end
end
if looksFresh then
hardResetRound("new round detected")
S.pending=nil
else
for v in pairs(oldKnown) do
if not newKnown[v] then
S.ledger[v]=true
end
end
end
S.myHand=newMy
S.oppHand=newOpp
S.hasSnapshot=true
S.lastAddAt=now()
local p=S.pending
S.pending=nil
if p then
if now()-p.t0>2.5 then
log("Pending inference expired")
elseif p.type=="ncard" then
local n=p.n
if Infer.inMine(n) then
log(
("N-Card(%d): in your hand")
:format(n)
)
elseif Infer.inOppVis(n) then
log(
("N-Card(%d): opponent visible")
:format(n)
)
else
Infer.deduceOpp(
n,
"N-Card was not drawn"
)
end
elseif p.type=="perfect" then
local d=p.need
if type(d)=="number"
and d>=1
and d<=11
and not Infer.inMine(d)
and not Infer.inOppVis(d) then
Infer.deduceOpp(
d,
"Perfect Draw did not reach target"
)
end
end
end
Infer.recompute()
log(
(
"AddCards: You %d Opponent %d Husk %d "
..
"pool={%s}"
):format(
#S.myHand,
#S.oppHand,
S.oppHusk,
table.concat(S.pool,",")
)
)
Bus.emit("refresh")
end
function Core.onPlayed(x,y)
local s1,s2
if y==nil and type(x)=="table" then
s1,s2=x[1],x[2]
else
s1,s2=x,y
end
local newMe,newOpp=normTrumpList(s1),normTrumpList(s2)
for _,t in ipairs(diffAdded(S.tableMe,newMe)) do
Bus.emit("mePlay",t)
end
for _,t in ipairs(diffAdded(S.tableOpp,newOpp)) do
S.oppPlays=S.oppPlays+1
log(("Opponent played %s"):format(t[1]))
end
S.tableMe,S.tableOpp=newMe,newOpp
Bus.emit("refresh")
end
function Core.onTrumps(l)
S.myTrumps=l or {}
Bus.emit("refresh")
end
function Core.onAddVisual(who)
if not (typeof(who)=="Instance" and who==LP) then
S.oppGain=S.oppGain+1
end
Bus.emit("refresh")
end
function Core.onClear()
S.round=S.round+1
S.myHand={}
S.oppHand={}
S.tableMe={}
S.tableOpp={}
S.deduce={}
S.user={}
S.ledger={}
S.pending=nil
S.oppGain=0
S.oppPlays=0
S.hasSnapshot=false
S.lastAddAt=0
S.roundEnded=false
Infer.recompute()
log(
("=== Round %d / ClearTable ===")
:format(S.round)
)
Bus.emit("refresh")
end
function Core.onBet(a)
if type(a)=="table" then
S.bet=a[1]
else
S.bet=a
end
Bus.emit("refresh")
end
function Core.onScore(n)
S.target=n
Bus.emit("refresh")
end
function Core.onHidden(arg)
local opp=arg and arg[2]
if opp then
local s,t=0,{}
for _,c in ipairs(opp) do
s=s+(tonumber(c[1]) or 0)
t[#t+1]=
tostring(c[1])
..
(c[2] and "H" or "")
end
log(
("Reveal: Opponent=%s (total %d) You=%d")
:format(
table.concat(t,"+"),
s,
Infer.mySum()
)
)
end
S.roundEnded=true
Bus.emit("refresh")
end
Bus.on("mePlay",function(t)
local m=t[1] and t[1]:match("^(%d)%-Card$")
if m then
S.pending={
type="ncard",
n=tonumber(m),
t0=now()
}
elseif t[1]=="Perfect Draw" then
S.pending={
type="perfect",
need=S.target-Infer.mySum(),
t0=now()
}
end
end)
-- ============================ UI ============================
local UI={
dirty=true,
tint={},
textObjects={},
dynamicConnections={},
mouseInside=false,
opened=true,
}
local function rgbHex(c)
return string.format(
"#%02X%02X%02X",
math.floor(c.R*255+0.5),
math.floor(c.G*255+0.5),
math.floor(c.B*255+0.5)
)
end
local function richColor(text,color)
return (
'<font color="%s">%s</font>'
):format(
rgbHex(color),
tostring(text)
)
end
local function richEnemyHuskCandidates()
local hc=Infer.huskCands()
if #hc==0 then
return "—"
end
local out={}
for _,v in ipairs(hc) do
local result,_,_,kind=
Infer.evalEnemyHusk(v)
local color
if kind=="mixed" then
color=Color3.fromRGB(105,155,225)
elseif result and result>0 then
color=Color3.fromRGB(70,220,125)
elseif result and result<0 then
color=Color3.fromRGB(245,90,90)
elseif result==0 then
color=Color3.fromRGB(245,190,65)
else
color=TH.dim
end
out[#out+1]=richColor(
"<b>"..tostring(v).."</b>",
color
)
end
return table.concat(out," ")
end
local function richHuskCombinations(maxShow)
maxShow=maxShow or 18
local combos=Infer.huskCombinations()
if #combos==0 then
return "—"
end
local out={}
for i,combo in ipairs(combos) do
if i>maxShow then
out[#out+1]=(
"…(+%d)"
):format(
#combos-maxShow
)
break
end
local col
if combo.result>0 then
col=Color3.fromRGB(70,220,125)
elseif combo.result<0 then
col=Color3.fromRGB(245,90,90)
else
col=Color3.fromRGB(245,190,65)
end
out[#out+1]=richColor(
"<b>"
..
table.concat(combo.cards,"+")
..
"="
..
tostring(combo.total)
..
"</b>",
col
)
end
return table.concat(out," ")
end
local function new(class,props,parent)
local o=Instance.new(class)
for k,v in pairs(props or {}) do
o[k]=v
end
if parent then
o.Parent=parent
end
return o
end
local function regTint(o,baseTransparency)
UI.tint[#UI.tint+1]={
object=o,
base=baseTransparency or 0
}
return o
end
local function regText(o,offset,minSize,maxSize)
UI.textObjects[#UI.textObjects+1]={
object=o,
offset=offset or 0,
min=minSize or 9,
max=maxSize or 28
}
return o
end
local function round(o,r)
new("UICorner",{
CornerRadius=UDim.new(0,r or 6)
},o)
return o
end
local function stroke(o,color,thickness,transparency)
return new("UIStroke",{
Color=color or TH.border,
Thickness=thickness or 1,
Transparency=transparency or 0,
},o)
end
local function padding(o,l,r,t,b)
return new("UIPadding",{
PaddingLeft=UDim.new(0,l or 0),
PaddingRight=UDim.new(0,r or l or 0),
PaddingTop=UDim.new(0,t or 0),
PaddingBottom=UDim.new(0,b or t or 0),
},o)
end
local function textLabel(parent,props,offset)
props=props or {}
local p={
BackgroundTransparency=1,
BorderSizePixel=0,
Text="",
Font=Enum.Font.Gotham,
TextSize=CFG.fontSize+(offset or 0),
TextColor3=TH.text,
TextXAlignment=Enum.TextXAlignment.Left,
TextYAlignment=Enum.TextYAlignment.Center,
}
for k,v in pairs(props) do
p[k]=v
end
local o=new("TextLabel",p,parent)
regText(o,offset or 0)
return o
end
local function makeButton(parent,props,offset)
props=props or {}
local p={
BackgroundColor3=TH.surface2,
BorderSizePixel=0,
AutoButtonColor=false,
Active=true,
Text="",
Font=Enum.Font.GothamMedium,
TextSize=CFG.fontSize+(offset or 0),
TextColor3=TH.text,
}
for k,v in pairs(props) do
p[k]=v
end
local b=new("TextButton",p,parent)
regText(b,offset or 0)
return b
end
local function setHover(button,normal,hover)
button.MouseEnter:Connect(function()
if button.Parent then
button.BackgroundColor3=hover
end
end)
button.MouseLeave:Connect(function()
if button.Parent then
button.BackgroundColor3=normal
end
end)
end
-- ---------------------------------------------------------
-- Mouse blocker
--
-- The transparent button consumes mouse input within the sidebar.
-- Actual controls use a higher ZIndex and remain clickable.
-- ---------------------------------------------------------
local function installInputBlocker(parent)
local blocker=new("TextButton",{
Name="__InputBlocker",
Size=UDim2.fromScale(1,1),
Position=UDim2.fromScale(0,0),
BackgroundTransparency=1,
BorderSizePixel=0,
Text="",
AutoButtonColor=false,
Active=true,
Selectable=false,
ZIndex=1,
},parent)
blocker.MouseButton1Down:Connect(function()
end)
blocker.MouseButton2Down:Connect(function()
end)
blocker.MouseEnter:Connect(function()
UI.mouseInside=true
end)
blocker.MouseLeave:Connect(function()
UI.mouseInside=false
end)
return blocker
end
local function chip(parent,text,col,order)
local c=new("Frame",{
Size=UDim2.fromOffset(34,28),
BackgroundColor3=col,
BorderSizePixel=0,
LayoutOrder=order,
Active=true,
ZIndex=8,
},parent)
round(c,6)
regText(
textLabel(c,{
Size=UDim2.fromScale(1,1),
Text=text,
Font=Enum.Font.GothamBold,
TextColor3=TH.white,
TextXAlignment=Enum.TextXAlignment.Center,
ZIndex=9,
},-1),
0
)
return c
end
function UI.updateFonts()
for _,entry in ipairs(UI.textObjects) do
local o=entry.object
if o and o.Parent then
o.TextSize=math.clamp(
CFG.fontSize+entry.offset,
entry.min,
entry.max
)
end
end
if UI.fontPct then
UI.fontPct.Text=tostring(CFG.fontSize).."px"
end
UI.layoutGrid()
end
function UI.applyOpacity()
local p=math.clamp(CFG.opacity,0.35,1)
for _,entry in ipairs(UI.tint) do
local o=entry.object
if o and o.Parent then
local base=entry.base or 0
o.BackgroundTransparency=
base+(1-base)*(1-p)
end
end
end
function UI.layoutGrid()
if not UI.grid or not UI.gridLayout then
return
end
local absolute=UI.grid.AbsoluteSize.X
if absolute<=10 then
absolute=CFG.width-36
end
local gap=4
local totalGap=gap*10
local cellW=math.floor((absolute-totalGap)/11)
cellW=math.max(24,cellW)
local cellH=math.clamp(
49+(CFG.fontSize-15)*2,
44,
62
)
UI.gridLayout.CellPadding=UDim2.fromOffset(gap,0)
UI.gridLayout.CellSize=UDim2.fromOffset(cellW,cellH)
UI.grid.Size=UDim2.new(1,0,0,cellH)
for _,entry in pairs(UI.cells or {}) do
if entry.val then
entry.val.TextSize=math.clamp(
CFG.fontSize,
12,
20
)
end
if entry.up then
entry.up.TextSize=math.clamp(
CFG.fontSize-4,
9,
15
)
end
if entry.dn then
entry.dn.TextSize=math.clamp(
CFG.fontSize-4,
9,
15
)
end
end
end
function UI.toggleOpen()
if not UI.bar then
return
end
local closing=UI.bar.Visible
if closing then
UI.bar.Visible=false
if UI.shadow then
UI.shadow.Visible=false
end
if UI.tab then
UI.tab.Visible=true
end
UI.opened=false
UI.mouseInside=false
else
UI.bar.Visible=true
if UI.shadow then
UI.shadow.Visible=true
end
if UI.tab then
UI.tab.Visible=false
end
UI.opened=true
task.defer(function()
UI.layoutGrid()
end)
end
end
function UI.build()
-- ====================== GUI ROOT ======================
local g=new("ScreenGui",{
Name="T21",
DisplayOrder=2147483647,
IgnoreGuiInset=true,
ResetOnSpawn=false,
ZIndexBehavior=Enum.ZIndexBehavior.Sibling,
})
pcall(function()
g.Parent=game:GetService("CoreGui")
end)
if not g.Parent then
g.Parent=LP:WaitForChild("PlayerGui")
end
UI.gui=g
-- ====================== CURSOR ======================
local cg=new("ScreenGui",{
Name="T21_Cursor",
DisplayOrder=2147483647,
IgnoreGuiInset=true,
ResetOnSpawn=false,
})
pcall(function()
cg.Parent=game:GetService("CoreGui")
end)
if not cg.Parent then
cg.Parent=LP:WaitForChild("PlayerGui")
end
local cur=new("ImageLabel",{
Size=UDim2.fromOffset(24,24),
BackgroundTransparency=1,
Image=CFG.cursor,
ZIndex=10000,
},cg)
UI.cursorGui=cg
UI.cursor=cur
-- ====================== SIDEBAR ======================
local shadow=new("Frame",{
Name="Shadow",
AnchorPoint=Vector2.new(1,0),
Position=UDim2.new(1,-1,0,0),
Size=UDim2.new(0,CFG.width+8,1,0),
BackgroundColor3=Color3.new(0,0,0),
BackgroundTransparency=0.40,
BorderSizePixel=0,
ZIndex=0,
},g)
UI.shadow=shadow
local bar=new("Frame",{
Name="Bar",
AnchorPoint=Vector2.new(1,0),
Position=UDim2.new(1,0,0,0),
Size=UDim2.new(0,CFG.width,1,0),
BackgroundColor3=TH.bg,
BorderSizePixel=0,
Active=true,
ClipsDescendants=false,
ZIndex=1,
},g)
UI.bar=bar
regTint(bar,0)
stroke(bar,TH.border,1,0)
installInputBlocker(bar)
local contentRoot=new("Frame",{
Name="Content",
Size=UDim2.fromScale(1,1),
BackgroundTransparency=1,
BorderSizePixel=0,
Active=true,
ZIndex=3,
},bar)
UI.contentRoot=contentRoot
-- ====================== RESIZE EDGE ======================
local rz=new("TextButton",{
Name="Resize",
Size=UDim2.new(0,10,1,0),
Position=UDim2.new(0,-5,0,0),
BackgroundColor3=TH.white,
BackgroundTransparency=1,
BorderSizePixel=0,
Text="",
Active=true,
AutoButtonColor=false,
ZIndex=50,
},bar)
local edge=new("Frame",{
AnchorPoint=Vector2.new(0.5,0),
Position=UDim2.new(0.5,0,0,0),
Size=UDim2.new(0,1,1,0),
BackgroundColor3=TH.border2,
BorderSizePixel=0,
ZIndex=51,
},rz)
local rsz=false
rz.MouseEnter:Connect(function()
edge.BackgroundColor3=TH.white
end)
rz.MouseLeave:Connect(function()
if not rsz then
edge.BackgroundColor3=TH.border2
end
end)
rz.InputBegan:Connect(function(i)
if i.UserInputType==Enum.UserInputType.MouseButton1 then
rsz=true
edge.BackgroundColor3=TH.white
end
end)
local resizeChanged=UIS.InputChanged:Connect(function(i)
if rsz and i.UserInputType==Enum.UserInputType.MouseMovement then
local vp=workspace.CurrentCamera
and workspace.CurrentCamera.ViewportSize.X
or 1920
CFG.width=math.clamp(
vp-i.Position.X,
CFG.minW,
CFG.maxW
)
bar.Size=UDim2.new(0,CFG.width,1,0)
shadow.Size=UDim2.new(0,CFG.width+8,1,0)
task.defer(function()
UI.layoutGrid()
end)
end
end)
local resizeEnded=UIS.InputEnded:Connect(function(i)
if i.UserInputType==Enum.UserInputType.MouseButton1 then
rsz=false
edge.BackgroundColor3=TH.border2
end
end)
UI.dynamicConnections[#UI.dynamicConnections+1]=resizeChanged
UI.dynamicConnections[#UI.dynamicConnections+1]=resizeEnded
-- ====================== HEADER ======================
local head=new("Frame",{
Size=UDim2.new(1,0,0,58),
BackgroundColor3=TH.surface,
BorderSizePixel=0,
Active=true,
ZIndex=5,
},contentRoot)
regTint(head,0)
new("Frame",{
Size=UDim2.new(1,0,0,1),
Position=UDim2.new(0,0,1,-1),
BackgroundColor3=TH.border,
BorderSizePixel=0,
ZIndex=6,
},head)
local brand=textLabel(head,{
Size=UDim2.new(1,-54,0,25),
Position=UDim2.new(0,16,0,9),
Text="TWENTY-ONE",
Font=Enum.Font.GothamBold,
TextColor3=TH.white,
ZIndex=7,
},2)
local subtitle=textLabel(head,{
Size=UDim2.new(1,-54,0,16),
Position=UDim2.new(0,16,0,34),
Text="ASSIST / SIDEBAR",
Font=Enum.Font.GothamMedium,
TextColor3=TH.dim,
ZIndex=7,
},-4)
local fold=makeButton(head,{
Size=UDim2.fromOffset(34,34),
Position=UDim2.new(1,-44,0,12),
BackgroundColor3=TH.surface2,
Text="◀",
TextColor3=TH.text2,
ZIndex=8,
},0)
round(fold,7)
stroke(fold,TH.border,1,0)
setHover(fold,TH.surface2,TH.hover)
fold.MouseButton1Click:Connect(function()
UI.toggleOpen()
end)
-- ====================== COLLAPSED TAB ======================
local tab=makeButton(g,{
Name="Tab",
AnchorPoint=Vector2.new(1,0),
Position=UDim2.new(1,0,0,150),
Size=UDim2.fromOffset(34,126),
BackgroundColor3=TH.surface,
Text="◀\n\nT\n2\n1",
TextColor3=TH.white,
Visible=false,
Active=true,
ZIndex=100,
},-2)
round(tab,8)
stroke(tab,TH.border2,1,0)
setHover(tab,TH.surface,TH.hover)
tab.MouseButton1Click:Connect(function()
UI.toggleOpen()
end)
UI.tab=tab
-- ====================== SETTINGS ======================
local settings=new("Frame",{
Size=UDim2.new(1,-24,0,72),
Position=UDim2.new(0,12,0,68),
BackgroundColor3=TH.surface,
BorderSizePixel=0,
Active=true,
ZIndex=5,
},contentRoot)
round(settings,8)
stroke(settings,TH.border,1,0)
regTint(settings,0)
local function slider(
y,
title,
minValue,
maxValue,
getValue,
setValue,
formatter
)
textLabel(settings,{
Size=UDim2.fromOffset(64,26),
Position=UDim2.new(0,12,0,y),
Text=title,
Font=Enum.Font.GothamMedium,
TextColor3=TH.dim,
ZIndex=8,
},-2)
local track=new("Frame",{
Size=UDim2.new(1,-152,0,6),
Position=UDim2.new(0,76,0,y+10),
BackgroundColor3=TH.surface2,
BorderSizePixel=0,
Active=true,
ZIndex=8,
},settings)
round(track,99)
local value=getValue()
local ratio=math.clamp(
(value-minValue)/(maxValue-minValue),
0,
1
)
local fill=new("Frame",{
Size=UDim2.new(ratio,0,1,0),
BackgroundColor3=TH.white,
BorderSizePixel=0,
ZIndex=9,
},track)
round(fill,99)
local knob=new("Frame",{
AnchorPoint=Vector2.new(0.5,0.5),
Position=UDim2.new(ratio,0,0.5,0),
Size=UDim2.fromOffset(12,12),
BackgroundColor3=TH.white,
BorderSizePixel=0,
ZIndex=10,
},track)
round(knob,99)
stroke(knob,TH.bg,2,0)
local val=textLabel(settings,{
Size=UDim2.fromOffset(58,26),
Position=UDim2.new(1,-66,0,y),
Text=formatter(value),
Font=Enum.Font.GothamBold,
TextColor3=TH.text,
TextXAlignment=Enum.TextXAlignment.Right,
ZIndex=8,
},-2)
local dragging=false
local function updateFromX(x)
local w=track.AbsoluteSize.X
if w<=0 then
return
end
local p=math.clamp(
(x-track.AbsolutePosition.X)/w,
0,
1
)
local nv=minValue+(maxValue-minValue)*p
setValue(nv)
local actual=getValue()
local ar=math.clamp(
(actual-minValue)/(maxValue-minValue),
0,
1
)
fill.Size=UDim2.new(ar,0,1,0)
knob.Position=UDim2.new(ar,0,0.5,0)
val.Text=formatter(actual)
end
track.InputBegan:Connect(function(i)
if i.UserInputType==Enum.UserInputType.MouseButton1
or i.UserInputType==Enum.UserInputType.Touch then
dragging=true
updateFromX(i.Position.X)
end
end)
knob.InputBegan:Connect(function(i)
if i.UserInputType==Enum.UserInputType.MouseButton1
or i.UserInputType==Enum.UserInputType.Touch then
dragging=true
updateFromX(i.Position.X)
end
end)
local changed=UIS.InputChanged:Connect(function(i)
if dragging
and (
i.UserInputType==Enum.UserInputType.MouseMovement
or i.UserInputType==Enum.UserInputType.Touch
) then
updateFromX(i.Position.X)
end
end)
local ended=UIS.InputEnded:Connect(function(i)
if i.UserInputType==Enum.UserInputType.MouseButton1
or i.UserInputType==Enum.UserInputType.Touch then
dragging=false
end
end)
UI.dynamicConnections[#UI.dynamicConnections+1]=changed
UI.dynamicConnections[#UI.dynamicConnections+1]=ended
return {
track=track,
fill=fill,
knob=knob,
value=val,
}
end
local opacitySlider=slider(
8,
"Opacity",
0.35,
1,
function()
return CFG.opacity
end,
function(v)
CFG.opacity=math.clamp(v,0.35,1)
UI.applyOpacity()
end,
function(v)
return math.floor(v*100+0.5).."%"
end
)
local fontSlider=slider(
38,
"Text Size",
CFG.minFont,
CFG.maxFont,
function()
return CFG.fontSize
end,
function(v)
CFG.fontSize=math.clamp(
math.floor(v+0.5),
CFG.minFont,
CFG.maxFont
)
UI.updateFonts()
end,
function(v)
return math.floor(v+0.5).."px"
end
)
UI.fontPct=fontSlider.value
-- ====================== SCROLLING BODY ======================
local body=new("ScrollingFrame",{
Size=UDim2.new(1,-16,1,-154),
Position=UDim2.new(0,8,0,148),
BackgroundTransparency=1,
BorderSizePixel=0,
ScrollBarThickness=3,
ScrollBarImageColor3=TH.border2,
AutomaticCanvasSize=Enum.AutomaticSize.Y,
CanvasSize=UDim2.new(),
Active=true,
ZIndex=5,
},contentRoot)
padding(body,4,7,0,12)
new("UIListLayout",{
Padding=UDim.new(0,10),
SortOrder=Enum.SortOrder.LayoutOrder,
},body)
UI.body=body
-- ====================== SECTIONS ======================
local function section(order,title)
local wrap=new("Frame",{
Size=UDim2.new(1,0,0,0),
AutomaticSize=Enum.AutomaticSize.Y,
BackgroundTransparency=1,
LayoutOrder=order,
Active=true,
ZIndex=6,
},body)
new("UIListLayout",{
Padding=UDim.new(0,5),
SortOrder=Enum.SortOrder.LayoutOrder,
},wrap)
local h=makeButton(wrap,{
Size=UDim2.new(1,0,0,34),
BackgroundColor3=TH.surface,
Text="",
LayoutOrder=1,
ZIndex=7,
},0)
round(h,8)
stroke(h,TH.border,1,0)
regTint(h,0)
setHover(h,TH.surface,TH.hover)
local marker=new("Frame",{
Size=UDim2.fromOffset(3,14),
Position=UDim2.new(0,10,0.5,-7),
BackgroundColor3=TH.white,
BorderSizePixel=0,
ZIndex=8,
},h)
round(marker,99)
textLabel(h,{
Size=UDim2.new(1,-56,1,0),
Position=UDim2.new(0,22,0,0),
Text=string.upper(title),
Font=Enum.Font.GothamBold,
TextColor3=TH.text,
ZIndex=8,
},-1)
local arr=textLabel(h,{
Size=UDim2.fromOffset(28,34),
Position=UDim2.new(1,-34,0,0),
Text="▾",
Font=Enum.Font.GothamBold,
TextColor3=TH.dim,
TextXAlignment=Enum.TextXAlignment.Center,
ZIndex=8,
},0)
local content=new("Frame",{
Size=UDim2.new(1,0,0,0),
AutomaticSize=Enum.AutomaticSize.Y,
BackgroundTransparency=1,
LayoutOrder=2,
Active=true,
ZIndex=7,
},wrap)
padding(content,5,5,2,2)
new("UIListLayout",{
Padding=UDim.new(0,7),
SortOrder=Enum.SortOrder.LayoutOrder,
},content)
local open=true
h.MouseButton1Click:Connect(function()
open=not open
content.Visible=open
arr.Text=open and "▾" or "▸"
end)
return content
end
local function txt(parent,order)
local card=new("Frame",{
Size=UDim2.new(1,0,0,0),
AutomaticSize=Enum.AutomaticSize.Y,
BackgroundColor3=TH.surface,
BorderSizePixel=0,
LayoutOrder=order,
Active=true,
ZIndex=7,
},parent)
round(card,7)
stroke(card,TH.border,1,0)
regTint(card,0)
padding(card,11,11,9,9)
local label=textLabel(card,{
Size=UDim2.new(1,0,0,0),
AutomaticSize=Enum.AutomaticSize.Y,
Text="",
RichText=true,
Font=Enum.Font.Code,
TextColor3=TH.text2,
TextWrapped=true,
TextXAlignment=Enum.TextXAlignment.Left,
TextYAlignment=Enum.TextYAlignment.Top,
ZIndex=8,
},0)
return label
end
-- ====================== DECK ======================
local deckSec=section(1,"DECK")
local gridCard=new("Frame",{
Size=UDim2.new(1,0,0,0),
AutomaticSize=Enum.AutomaticSize.Y,
BackgroundColor3=TH.surface,
BorderSizePixel=0,
LayoutOrder=1,
Active=true,
ZIndex=7,
},deckSec)
round(gridCard,8)
stroke(gridCard,TH.border,1,0)
regTint(gridCard,0)
padding(gridCard,8,8,8,8)
local grid=new("Frame",{
Size=UDim2.new(1,0,0,52),
BackgroundTransparency=1,
Active=true,
ZIndex=8,
},gridCard)
local gridLayout=new("UIGridLayout",{
CellSize=UDim2.fromOffset(30,52),
CellPadding=UDim2.fromOffset(4,0),
FillDirection=Enum.FillDirection.Horizontal,
FillDirectionMaxCells=11,
HorizontalAlignment=Enum.HorizontalAlignment.Center,
VerticalAlignment=Enum.VerticalAlignment.Center,
SortOrder=Enum.SortOrder.LayoutOrder,
},grid)
UI.grid=grid
UI.gridLayout=gridLayout
UI.cells={}
for v=1,11 do
local cell=new("Frame",{
BackgroundColor3=TH.gray,
BorderSizePixel=0,
LayoutOrder=v,
Active=true,
ZIndex=9,
},grid)
round(cell,6)
stroke(cell,TH.border2,1,0.25)
local up=makeButton(cell,{
Size=UDim2.new(1,0,0,14),
Position=UDim2.new(0,0,0,0),
BackgroundTransparency=1,
Text="▲",
TextColor3=Color3.fromRGB(180,180,180),
ZIndex=11,
},-4)
local val=textLabel(cell,{
Size=UDim2.new(1,0,1,-28),
Position=UDim2.new(0,0,0,14),
Text=tostring(v),
Font=Enum.Font.GothamBold,
TextColor3=TH.white,
TextXAlignment=Enum.TextXAlignment.Center,
ZIndex=10,
},0)
local dn=makeButton(cell,{
Size=UDim2.new(1,0,0,14),
Position=UDim2.new(0,0,1,-14),
BackgroundTransparency=1,
Text="▼",
TextColor3=Color3.fromRGB(180,180,180),
ZIndex=11,
},-4)
local function cyc(dir)
local order_={
"auto",
"husk",
"deck"
}
local cur=S.user[v] or "auto"
local idx=1
for i,st in ipairs(order_) do
if st==cur then
idx=i
end
end
idx=idx+dir
if idx<1 then
idx=#order_
elseif idx>#order_ then
idx=1
end
S.user[v]=
(order_[idx]=="auto")
and nil
or order_[idx]
Infer.recompute()
Bus.emit("refresh")
end
up.MouseButton1Click:Connect(function()
cyc(-1)
end)
dn.MouseButton1Click:Connect(function()
cyc(1)
end)
UI.cells[v]={
cell=cell,
val=val,
up=up,
dn=dn,
}
end
UI.deckText=txt(deckSec,2)
-- ====================== HANDS ======================
local handSec=section(2,"HANDS")
local handCard=new("Frame",{
Size=UDim2.new(1,0,0,0),
AutomaticSize=Enum.AutomaticSize.Y,
BackgroundColor3=TH.surface,
BorderSizePixel=0,
LayoutOrder=1,
Active=true,
ZIndex=7,
},handSec)
round(handCard,8)
stroke(handCard,TH.border,1,0)
regTint(handCard,0)
padding(handCard,10,10,9,9)
new("UIListLayout",{
Padding=UDim.new(0,7),
SortOrder=Enum.SortOrder.LayoutOrder,
},handCard)
local function row(order,label,color)
local r=new("Frame",{
Size=UDim2.new(1,0,0,30),
BackgroundTransparency=1,
LayoutOrder=order,
Active=true,
ZIndex=8,
},handCard)
local name=textLabel(r,{
Size=UDim2.fromOffset(40,30),
Text=label,
Font=Enum.Font.GothamBold,
TextColor3=color,
ZIndex=9,
},0)
local host=new("Frame",{
Size=UDim2.new(1,-44,1,0),
Position=UDim2.new(0,44,0,0),
BackgroundTransparency=1,
Active=true,
ZIndex=9,
},r)
new("UIListLayout",{
FillDirection=Enum.FillDirection.Horizontal,
Padding=UDim.new(0,5),
VerticalAlignment=Enum.VerticalAlignment.Center,
SortOrder=Enum.SortOrder.LayoutOrder,
},host)
return host
end
UI.myRow=row(1,"YOU",TH.green)
UI.oppRow=row(2,"OPP",TH.red)
-- ====================== REMAINING SECTIONS ======================
UI.anaText=
txt(section(3,"ANALYSIS"),1)
UI.huskText=
txt(section(4,"HUSK"),1)
UI.advText=
txt(section(5,"ADVICE"),1)
UI.trumpText=
txt(section(6,"TRUMPS"),1)
UI.logText=
txt(section(7,"LOG"),1)
UI.layoutGrid()
UI.updateFonts()
UI.applyOpacity()
task.defer(function()
UI.layoutGrid()
end)
end
function UI.renderHands()
for _,h in ipairs({
UI.myRow,
UI.oppRow
}) do
for _,c in ipairs(h:GetChildren()) do
if c:IsA("Frame") then
c:Destroy()
end
end
end
for i,c in ipairs(S.myHand) do
chip(
UI.myRow,
tostring(c.v or "?"),
c.husk and TH.greenD or TH.green,
i
)
end
for i,c in ipairs(S.oppHand) do
if c.husk then
chip(
UI.oppRow,
"?",
TH.gold,
i
)
else
chip(
UI.oppRow,
tostring(c.v or "?"),
TH.red,
i
)
end
end
end
function UI.refresh()
if not UI.gui or not UI.bar then
return
end
-- ====================== DECK STATE ======================
for v=1,11 do
local col=TH.gray
if Infer.inMine(v) then
col=TH.green
elseif Infer.inOppVis(v) then
col=TH.red
elseif S.deduce[v]
or S.user[v]=="husk" then
col=Color3.fromRGB(112,76,12)
elseif S.user[v]=="deck" then
col=TH.blue
end
UI.cells[v].cell.BackgroundColor3=col
end
UI.renderHands()
local my=Infer.mySum()
local T=S.target
local need=T-my
local cands=Infer.deckCands()
local hc=Infer.huskCands()
local mn,mx=Infer.oppRange()
-- ====================== DECK ======================
UI.deckText.Text=(
"Pool %s\n"
..
"Deck Candidates %s"
):format(
#S.pool>0
and table.concat(S.pool,", ")
or "—",
#cands>0
and table.concat(cands,", ")
or "—"
)
-- ====================== ANALYSIS ======================
local mid=(mn+mx)/2
UI.anaText.Text=table.concat({
("You / Hand Total %d / %d"):format(my,T),
("Target Progress %d%%"):format(
math.floor(
math.clamp(
T~=0 and my/T or 0,
0,
1
)*100
)
),
("Perfect Draw %s"):format(
need>=1 and need<=11
and tostring(need)
or "—"
),
"",
("Opponent / Visible %d"):format(S.oppVis),
("Opponent / Husk × %d"):format(S.oppHusk),
("Opponent / Range %d ~ %d"):format(mn,mx),
("Opponent / Midpoint %d"):format(
math.floor(mid)
),
("Opponent / Progress %d%%"):format(
math.floor(
math.clamp(
T~=0 and mid/T or 0,
0,
1
)*100
)
),
"",
("Bet %s"):format(
S.bet and tostring(S.bet) or "—"
),
},"\n")
-- ====================== HUSK ======================
local dl={}
for v,w in pairs(S.deduce) do
dl[#dl+1]=("%d (%s)"):format(v,w)
end
table.sort(dl)
local assertions={}
for v,st in pairs(S.user) do
assertions[#assertions+1]={
v=v,
text=("%d=%s"):format(v,st)
}
end
table.sort(assertions,function(a,b)
return a.v<b.v
end)
local assertionText={}
for _,entry in ipairs(assertions) do
assertionText[#assertionText+1]=entry.text
end
local outcome=Infer.enemyHuskOutcome()
local resultText="—"
if outcome.count>0 then
resultText=(
'<font color="#46DC7D">Opp Win %d</font>'
..
' / '
..
'<font color="#F5BE41">Tie %d</font>'
..
' / '
..
'<font color="#F55A5A">Opp Lose %d</font>'
):format(
#outcome.win,
#outcome.tie,
#outcome.lose
)
end
local huskLines={
"Husk Candidates "..richEnemyHuskCandidates(),
("Range %s ~ %s"):format(
#hc>0 and hc[1] or "—",
#hc>0 and hc[#hc] or "—"
),
"Results "..resultText,
}
if S.oppHusk>=2 then
huskLines[#huskLines+1]=
"Combinations "
..
richHuskCombinations(18)
end
huskLines[#huskLines+1]=(
"Inferred %s"
):format(
#dl>0
and table.concat(dl,", ")
or "—"
)
huskLines[#huskLines+1]=(
"Manual States %s"
):format(
#assertionText>0
and table.concat(assertionText,", ")
or "—"
)
UI.huskText.Text=
table.concat(
huskLines,
"\n"
)
-- ====================== ADVICE ======================
UI.advText.Text=
table.concat(
Infer.advise(),
"\n"
)
-- ====================== TRUMPS ======================
UI.trumpText.Text=(
"You %s\n"
..
"Opponent Gained %d / Played %d / Approx. Hand %d"
):format(
#S.myTrumps>0
and table.concat(S.myTrumps,", ")
or "—",
S.oppGain,
S.oppPlays,
math.max(
0,
S.oppGain-S.oppPlays
)
)
-- ====================== LOG ======================
local last={}
for i=math.max(1,#S.log-7),#S.log do
last[#last+1]=S.log[i]
end
UI.logText.Text=
#last>0
and table.concat(last,"\n")
or "No logs yet"
end
-- ============================ HOOKS ============================
local Events=RS:WaitForChild("Events",30)
local conns={}
local function hook(n,f)
local r=Events and Events:FindFirstChild(n)
if r then
conns[#conns+1]=
r.OnClientEvent:Connect(f)
end
end
hook("AddCards", Core.onAddCards)
hook("PlayedTrumpVisual", Core.onPlayed)
hook("UpdatePlayerTrumps", Core.onTrumps)
hook("AddingTrumpVisual", Core.onAddVisual)
hook("ClearTable", Core.onClear)
hook("ChangeVisualBet", Core.onBet)
hook("UpdateWinningScore", Core.onScore)
hook("ShowHiddenCards", Core.onHidden)
Bus.on("refresh",function()
UI.dirty=true
end)
local lastR=0
conns[#conns+1]=Run.Heartbeat:Connect(function()
local t=now()
if (
UI.dirty
or t-lastR>1
)
and t-lastR>0.15 then
UI.dirty=false
lastR=t
pcall(UI.refresh)
end
end)
-- ============================ DUMP ============================
local dumpBuf={}
local function ser(v,d,seen)
d=d or 0
seen=seen or {}
local ty=typeof(v)
if ty=="table" then
if seen[v] then
return "<c>"
end
if d>=6 then
return "..."
end
seen[v]=true
local p={}
local i=0
for k,val in pairs(v) do
i=i+1
if i>60 then
p[#p+1]="..."
break
end
p[#p+1]=
(type(k)=="number")
and ser(val,d+1,seen)
or (
tostring(k)
..
"="
..
ser(val,d+1,seen)
)
end
seen[v]=nil
return "{"..table.concat(p,",").."}"
elseif ty=="string" then
return string.format(
"%q",
#v>200
and v:sub(1,200).."..."
or v
)
elseif ty=="number" then
return v==math.floor(v)
and string.format("%d",v)
or string.format("%.6g",v)
elseif ty=="Instance" then
return v:GetFullName()
else
return tostring(v)
end
end
if Events then
for _,n in ipairs(CFG.dumpEvents) do
local r=Events:FindFirstChild(n)
if r then
conns[#conns+1]=
r.OnClientEvent:Connect(function(...)
local pk=table.pack(...)
local parts={}
for i=1,pk.n do
parts[i]=ser(pk[i])
end
dumpBuf[#dumpBuf+1]=
os.date("[%H:%M:%S] ")
..
n
..
"("
..
table.concat(parts,", ")
..
")"
if #dumpBuf>400 then
table.remove(dumpBuf,1)
end
end)
end
end
end
-- ============================ BUILD UI ============================
UI.build()
-- Custom cursor while the UI is active.
local cursorConn=Run.RenderStepped:Connect(function()
if not UI.cursor
or not UI.cursor.Parent then
return
end
local m=UIS:GetMouseLocation()
UI.cursor.Position=
UDim2.fromOffset(
m.X,
m.Y
)
local shouldShow=
UI.gui
and UI.gui.Enabled
and (
UI.bar.Visible
or UI.tab.Visible
)
UI.cursor.Visible=shouldShow
if shouldShow then
UIS.MouseIconEnabled=false
else
UIS.MouseIconEnabled=true
end
end)
conns[#conns+1]=cursorConn
-- ============================ KEYBOARD ============================
conns[#conns+1]=UIS.InputBegan:Connect(function(i,gp)
if i.UserInputType==Enum.UserInputType.MouseButton1
or i.UserInputType==Enum.UserInputType.MouseButton2 then
return
end
if gp then
return
end
if i.KeyCode==CFG.toggleKey then
UI.toggleOpen()
elseif i.KeyCode==CFG.dumpKey then
if setclipboard then
pcall(
setclipboard,
table.concat(dumpBuf,"\n")
)
end
print(
"[T21] Copied "
..
#dumpBuf
..
" entries"
)
elseif i.KeyCode==CFG.unloadKey then
if _G.T21_Unload then
_G.T21_Unload()
end
end
end)
task.defer(function()
UI.refresh()
UI.layoutGrid()
end)
log(
"Sidebar V1 loaded: "
..
"RightShift toggle / "
..
"F6 dump / "
..
"End unload"
)
-- ============================ UNLOAD ============================
_G.T21_Unload=function()
for _,c in ipairs(conns) do
pcall(function()
c:Disconnect()
end)
end
conns={}
for _,c in ipairs(UI.dynamicConnections) do
pcall(function()
c:Disconnect()
end)
end
UI.dynamicConnections={}
if UI.gui then
pcall(function()
UI.gui:Destroy()
end)
end
if UI.cursorGui then
pcall(function()
UI.cursorGui:Destroy()
end)
end
pcall(function()
UIS.MouseIconEnabled=true
end)
UI.gui=nil
UI.bar=nil
UI.cursorGui=nil
print("[T21] Unloaded")
endReview the source before using it. Executions count successful copies, capped at 10 per user and network per script in 24 hours.


Your community, in one place