local ADDON_NAME, RPAL = ...
local BG=RPAL.BoardGames
if not BG then return end

local FONT=STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF"
local TITLE_FONT="Fonts\\MORPHEUS.TTF"
local WHITE="Interface\\Buttons\\WHITE8X8"
local CELESTIAL="Interface\\AddOns\\ODYN\\Textures\\Celestial\\"
local BOARD_ASSET="Interface\\AddOns\\ODYN\\Textures\\BoardGames\\"
local ORNAMENT_ROYAL="Interface\\AddOns\\ODYN\\Textures\\Cosmetics\\ornament_royal_corner.tga"
local ORNAMENT_SEAL="Interface\\AddOns\\ODYN\\Textures\\Cosmetics\\ornament_royal_seal.tga"

local frame,matchFrame,challengeFrame,statusToast
local selectedGame="chess"
local selectedTab="play"
local selectedSquare

local GAME_PALETTE={
    chess={
        gold={.60,.79,1,1},pale={.90,.95,1,1},muted={.58,.68,.78,1},main={.045,.075,.12,1},panel={.07,.115,.17,1},panel2={.055,.09,.14,1},border={.24,.43,.62,1},accent={.42,.69,1,1},accent2={.52,.43,.88,1},
        title="CHESS",sub="ODYN STRATEGY TABLE",playHelp="Challenge another ODYN player to a full chess match, or practice against an ODYN bot. Your table half and piece set travel with you into online games."
    },
    checkers={
        gold={1,.54,.31,1},pale={1,.91,.84,1},muted={.76,.62,.54,1},main={.13,.047,.036,1},panel={.19,.070,.048,1},panel2={.155,.052,.040,1},border={.48,.20,.13,1},accent={1,.40,.20,1},accent2={.88,.27,.24,1},
        title="CHECKERS",sub="ODYN DRAUGHTS TABLE",playHelp="Challenge another ODYN player to checkers, or practice against an ODYN bot. Captures are mandatory and chained jumps continue automatically."
    }
}

local function P(game) return GAME_PALETTE[game or selectedGame] or GAME_PALETTE.chess end
local function Scaled(n) local v=RPAL.GetScaledFontSize and RPAL.GetScaledFontSize(n) or n;return v*1.08 end
local function Clamp(x,a,b) x=tonumber(x) or 0;if x<a then return a elseif x>b then return b end;return x end
local function Mix(a,b,t) return {a[1]+(b[1]-a[1])*t,a[2]+(b[2]-a[2])*t,a[3]+(b[3]-a[3])*t,1} end
local function Mul(a,k) return {Clamp(a[1]*k,0,1),Clamp(a[2]*k,0,1),Clamp(a[3]*k,0,1),a[4] or 1} end
local function Short(s,n) s=tostring(s or "");return #s>n and (s:sub(1,n-3).."...") or s end
local function Text(parent,text,size,color,font)
    local t=parent:CreateFontString(nil,"OVERLAY");t:SetFont(font or FONT,Scaled(math.max(7.4,tonumber(size) or 10)),"OUTLINE");local c=color or P().pale;t:SetTextColor(c[1],c[2],c[3],c[4] or 1);t:SetText(text or "");t:SetShadowColor(0,0,0,.75);t:SetShadowOffset(1,-1);return t
end
local function Backdrop(f,border,bg,alpha)
    local p=P();f:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=11,insets={left=2,right=2,top=2,bottom=2}})
    local c=bg or p.panel;f:SetBackdropColor(c[1],c[2],c[3],alpha or c[4] or .98);local e=border or p.border;f:SetBackdropBorderColor(e[1],e[2],e[3],e[4] or .95)
end
local function Btn(parent,label,w,h,accent)
    local p=P();local b=CreateFrame("Button",nil,parent,"BackdropTemplate");b:SetSize(w or 110,h or 30);Backdrop(b,accent and p.gold or p.border,accent and Mul(p.panel,1.18) or p.panel2,.99);b.label=Text(b,label,9.2,accent and p.gold or p.pale);b.label:SetPoint("CENTER",0,1)
    b:SetScript("OnEnter",function(self)local q=P();self:SetBackdropBorderColor(q.gold[1],q.gold[2],q.gold[3],1);local c=Mul(q.panel,1.35);self:SetBackdropColor(c[1],c[2],c[3],1)end)
    b:SetScript("OnLeave",function(self)local q=P();local e=accent and q.gold or q.border;self:SetBackdropBorderColor(e[1],e[2],e[3],.95);local c=accent and Mul(q.panel,1.18) or q.panel2;self:SetBackdropColor(c[1],c[2],c[3],.99)end)
    if RPAL.AttachButtonMotion then RPAL.AttachButtonMotion(b,.014) end
    if RPAL.AttachFocusMark then RPAL.AttachFocusMark(b,P().gold) end
    return b
end
local function Dynamic(f) f._bgDynamic=true;return f end
local function DText(parent,...) local t=Text(parent,...);t._bgDynamic=true;return t end
local function DBtn(parent,...) local b=Btn(parent,...);b._bgDynamic=true;return b end
local function Clear(parent)
    if not parent then return end
    for _,c in ipairs({parent:GetChildren()}) do if c._bgDynamic then c:Hide();c:SetParent(nil) end end
    for _,r in ipairs({parent:GetRegions()}) do if r._bgDynamic then r:Hide() end end
end
local function ShowStatus(msg)
    if not statusToast then
        statusToast=CreateFrame("Frame",nil,UIParent,"BackdropTemplate");statusToast:SetSize(450,46);statusToast:SetPoint("TOP",0,-132);statusToast:SetFrameStrata("TOOLTIP");statusToast.text=Text(statusToast,"",10,P().pale);statusToast.text:SetPoint("CENTER");statusToast:Hide()
    end
    local p=P();Backdrop(statusToast,p.gold,p.panel2,.99);statusToast.text:SetTextColor(p.pale[1],p.pale[2],p.pale[3],1);statusToast.text:SetText(tostring(msg or ""));statusToast:Show();statusToast:SetAlpha(1)
    if statusToast._timer and statusToast._timer.Cancel then statusToast._timer:Cancel() end
    if C_Timer and C_Timer.NewTimer then statusToast._timer=C_Timer.NewTimer(2.7,function()if statusToast then statusToast:Hide() end end) end
end
RPAL.BoardGameStatus=ShowStatus

local function DressPanel(panel,accent)
    accent=accent or P().gold
    local wash=panel:CreateTexture(nil,"BACKGROUND",nil,1);wash:SetTexture(WHITE);wash:SetPoint("TOPLEFT",3,-3);wash:SetPoint("TOPRIGHT",-3,-3);wash:SetHeight(58);wash:SetColorTexture(accent[1],accent[2],accent[3],.055)
    local top=panel:CreateTexture(nil,"BORDER");top:SetTexture(WHITE);top:SetPoint("TOPLEFT",9,-7);top:SetPoint("TOPRIGHT",-9,-7);top:SetHeight(2);top:SetColorTexture(accent[1],accent[2],accent[3],.5)
    local tl=panel:CreateTexture(nil,"BORDER");tl:SetTexture(ORNAMENT_ROYAL);tl:SetSize(54,54);tl:SetPoint("TOPLEFT",2,-2);tl:SetVertexColor(accent[1],accent[2],accent[3],.15)
end

local function EnsureMain()
    if frame then return frame end
    local f=CreateFrame("Frame","ODYN_BoardGames",UIParent,"BackdropTemplate");frame=f;f:SetSize(1240,790);f:SetPoint("CENTER");if RPAL.ApplyUILayer then RPAL.ApplyUILayer(f,"WORKSPACE")else f:SetFrameStrata("DIALOG");f:SetFrameLevel(320);f:SetToplevel(true)end;f:SetClampedToScreen(true);f:Hide();if RPAL.RegisterEscapeFrame then RPAL.RegisterEscapeFrame(f) end
    f:SetMovable(true);f:EnableMouse(true);f:RegisterForDrag("LeftButton");f:SetScript("OnDragStart",function(self)if IsAltKeyDown() then self:StartMoving() end end);f:SetScript("OnDragStop",function(self)self:StopMovingOrSizing()end)
    f:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=16,insets={left=4,right=4,top=4,bottom=4}})
    f.base=f:CreateTexture(nil,"BACKGROUND",nil,-8);f.base:SetTexture(WHITE);f.base:SetPoint("TOPLEFT",6,-6);f.base:SetPoint("BOTTOMRIGHT",-6,6)
    f.topWash=f:CreateTexture(nil,"BACKGROUND",nil,-7);f.topWash:SetTexture(WHITE);f.topWash:SetPoint("TOPLEFT",8,-8);f.topWash:SetPoint("TOPRIGHT",-8,-8);f.topWash:SetHeight(150)
    f.centerGlow=f:CreateTexture(nil,"BACKGROUND",nil,-6);f.centerGlow:SetTexture(CELESTIAL.."starlight_glow.tga");f.centerGlow:SetBlendMode("ADD");f.centerGlow:SetSize(1040,700);f.centerGlow:SetPoint("CENTER",0,-28)
    f.seal=f:CreateTexture(nil,"BACKGROUND",nil,-5);f.seal:SetTexture(ORNAMENT_SEAL);f.seal:SetSize(590,590);f.seal:SetPoint("CENTER",0,-36)
    f.headerLine=f:CreateTexture(nil,"BORDER");f.headerLine:SetTexture(WHITE);f.headerLine:SetPoint("TOPLEFT",22,-70);f.headerLine:SetPoint("TOPRIGHT",-22,-70);f.headerLine:SetHeight(2)
    local function Corner(point,x,y,rotation)local c=f:CreateTexture(nil,"BORDER");c:SetTexture(ORNAMENT_ROYAL);c:SetSize(132,132);c:SetPoint(point,x,y);if rotation and c.SetRotation then c:SetRotation(rotation)end;return c end
    f.corners={Corner("TOPLEFT",5,-5,0),Corner("TOPRIGHT",-5,-5,1.5707963),Corner("BOTTOMRIGHT",-5,5,3.1415926),Corner("BOTTOMLEFT",5,5,4.712389)}
    f.title=Text(f,"",24,P().gold,TITLE_FONT);f.title:SetPoint("TOPLEFT",34,-20);f.sub=Text(f,"",8.4,P().muted,FONT);f.sub:SetPoint("TOPLEFT",37,-53)
    f.modePlate=CreateFrame("Frame",nil,f,"BackdropTemplate");f.modePlate:SetSize(180,42);f.modePlate:SetPoint("TOPRIGHT",-58,-19);f.modeLabel=Text(f.modePlate,"ONLINE  •  BOT",9.2,P().gold,FONT);f.modeLabel:SetPoint("CENTER")
    local close=CreateFrame("Button",nil,f,"BackdropTemplate");close:SetSize(34,30);close:SetPoint("TOPRIGHT",-16,-16);close.label=Text(close,"×",18,P().gold,TITLE_FONT);close.label:SetPoint("CENTER",0,1);close:SetScript("OnClick",function()f:Hide()end);f.close=close
    f.nav=CreateFrame("Frame",nil,f,"BackdropTemplate");f.nav:SetPoint("TOPLEFT",24,-80);f.nav:SetPoint("TOPRIGHT",-24,-80);f.nav:SetHeight(48)
    local tabs={{"play","PLAY"},{"customization","CUSTOMIZATION"},{"history","HISTORY"}};f.tabs={}
    for i,t in ipairs(tabs) do
        local key,label=t[1],t[2];local b=CreateFrame("Button",nil,f.nav,"BackdropTemplate");b:SetSize(380,36);b:SetPoint("LEFT",12+(i-1)*390,0);b.label=Text(b,label,9.2,P().muted);b.label:SetPoint("CENTER",0,1);b.activeLine=b:CreateTexture(nil,"OVERLAY");b.activeLine:SetTexture(WHITE);b.activeLine:SetPoint("BOTTOMLEFT",8,4);b.activeLine:SetPoint("BOTTOMRIGHT",-8,4);b.activeLine:SetHeight(2);b:SetScript("OnClick",function()selectedTab=key;RPAL.RefreshBoardGame()end);f.tabs[key]=b
    end
    f.contentShell=CreateFrame("Frame",nil,f,"BackdropTemplate");f.contentShell:SetPoint("TOPLEFT",24,-140);f.contentShell:SetPoint("BOTTOMRIGHT",-24,22)
    f.content=CreateFrame("Frame",nil,f.contentShell);f.content:SetPoint("TOPLEFT",10,-10);f.content:SetPoint("BOTTOMRIGHT",-10,10)
    return f
end

local function RestyleMain()
    local f=EnsureMain();local p=P();f:SetBackdropColor(p.main[1]*.55,p.main[2]*.55,p.main[3]*.55,.998);f:SetBackdropBorderColor(p.gold[1],p.gold[2],p.gold[3],1);f.base:SetColorTexture(p.main[1],p.main[2],p.main[3],1);f.topWash:SetColorTexture(p.accent[1],p.accent[2],p.accent[3],.13);f.centerGlow:SetVertexColor(p.accent[1],p.accent[2],p.accent[3],.07);f.seal:SetVertexColor(p.gold[1],p.gold[2],p.gold[3],.035);f.headerLine:SetColorTexture(p.gold[1],p.gold[2],p.gold[3],.64)
    for _,c in ipairs(f.corners) do c:SetVertexColor(p.gold[1],p.gold[2],p.gold[3],.28) end
    f.title:SetText(p.title);f.title:SetTextColor(p.gold[1],p.gold[2],p.gold[3],1);f.sub:SetText(p.sub);f.sub:SetTextColor(p.muted[1],p.muted[2],p.muted[3],1)
    Backdrop(f.modePlate,p.gold,Mul(p.panel2,.65),.98);f.modeLabel:SetTextColor(p.gold[1],p.gold[2],p.gold[3],1)
    Backdrop(f.close,p.border,Mul(p.panel2,.55),.98);f.close.label:SetTextColor(p.gold[1],p.gold[2],p.gold[3],1)
    Backdrop(f.nav,p.border,Mul(p.main,.55),.96);Backdrop(f.contentShell,p.border,Mul(p.main,.72),.98)
    for key,b in pairs(f.tabs) do
        local on=key==selectedTab;Backdrop(b,on and p.gold or p.border,on and Mul(p.panel,1.08) or Mul(p.main,.75),.98);b.label:SetTextColor((on and p.gold or p.muted)[1],(on and p.gold or p.muted)[2],(on and p.gold or p.muted)[3],1);b.activeLine:SetColorTexture(p.gold[1],p.gold[2],p.gold[3],1);b.activeLine:SetAlpha(on and 1 or 0)
        b:SetScript("OnEnter",function(self)if key~=selectedTab then self:SetBackdropBorderColor(p.gold[1],p.gold[2],p.gold[3],.9)end end);b:SetScript("OnLeave",function(self)if key~=selectedTab then self:SetBackdropBorderColor(p.border[1],p.border[2],p.border[3],.95)end end)
    end
end

local function RenderPlay(content)
    local gs=BG.GetGameState(selectedGame);local p=P();local theme=BG.GetTheme(gs.theme);local set=BG.GetPieceSet(gs.pieceSet)
    local hero=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));hero:SetSize(520,180);hero:SetPoint("TOPLEFT",8,-8);Backdrop(hero,p.gold,p.panel,.98);DressPanel(hero,p.gold)
    local portrait=hero:CreateTexture(nil,"ARTWORK");portrait:SetSize(104,104);portrait:SetPoint("LEFT",18,6);if SetPortraitTexture then pcall(SetPortraitTexture,portrait,"player")else portrait:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")end
    local name=DText(hero,(RPAL.DisplayName and RPAL.DisplayName()) or UnitName("player"),20,p.gold,FONT);name:SetPoint("TOPLEFT",140,-26)
    local info=DText(hero,"Your character brings their own table half and piece set.",9,p.muted);info:SetPoint("TOPLEFT",142,-57)
    local tl=DText(hero,"TABLE THEME",8,p.gold);tl:SetPoint("TOPLEFT",142,-88);local tv=DText(hero,theme.name,11,p.pale,FONT);tv:SetPoint("TOPLEFT",142,-106)
    local pl=DText(hero,"PIECE SET  ·  "..set.name,8.7,p.muted);pl:SetPoint("TOPLEFT",142,-134)
    local custom=DBtn(hero,"Customize",116,28,false);custom:SetPoint("BOTTOMRIGHT",-12,12);custom:SetScript("OnClick",function()selectedTab="customization";RPAL.RefreshBoardGame()end)

    local setup=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));setup:SetSize(560,180);setup:SetPoint("TOPRIGHT",-8,-8);Backdrop(setup,p.border,p.panel2,.98);DressPanel(setup,p.accent)
    local sh=DText(setup,"TABLE RULES",11,p.gold,FONT);sh:SetPoint("TOPLEFT",16,-14)
    local lines
    if selectedGame=="chess" then lines={"Standard piece movement and check legality","Castling supported · promotion auto-queens","Checkmate and stalemate detection"}
    else lines={"Mandatory captures","Multi-jumps continue on the same turn","Back rank crowns a king"} end
    local y=-48;for _,line in ipairs(lines) do local dot=DText(setup,"•",10,p.gold);dot:SetPoint("TOPLEFT",18,y);local tx=DText(setup,line,9.2,p.pale);tx:SetPoint("TOPLEFT",34,y);y=y-31 end
    local split=DText(setup,"SPLIT TABLE",8,p.gold);split:SetPoint("TOPRIGHT",-18,-42);local spl=DText(setup,"Each player's four ranks use\ntheir own selected theme.",8.5,p.muted);spl:SetPoint("TOPRIGHT",-18,-62);spl:SetJustifyH("RIGHT")

    local play=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));play:SetPoint("TOPLEFT",8,-202);play:SetPoint("BOTTOMRIGHT",-8,8);Backdrop(play,p.border,p.panel,.98);DressPanel(play,p.gold)
    local ph=DText(play,"PLAY",16,p.gold,TITLE_FONT);ph:SetPoint("TOPLEFT",18,-16);local help=DText(play,p.playHelp,9,p.muted);help:SetPoint("TOPLEFT",18,-45);help:SetWidth(815);help:SetWordWrap(true)
    local challenge=DBtn(play,"Challenge Target",176,40,true);challenge:SetPoint("TOPLEFT",18,-96);challenge:SetScript("OnClick",function()local ok,msg=BG.ChallengeTarget(selectedGame);ShowStatus(msg or (ok and "Challenge sent." or "Could not challenge."))end)
    local active=BG.GetActiveMatch and BG.GetActiveMatch() or nil
    if active and not active.finished then
        local resume=DBtn(play,"Resume Active "..BG.GAMES[active.game].name,176,32,false);resume:SetPoint("LEFT",challenge,"RIGHT",10,0);resume:SetScript("OnClick",function()if RPAL.OpenBoardGameMatch then RPAL.OpenBoardGameMatch(active,active.viewerSide or 1)end end)
    end
    local practice=DBtn(play,"Practice vs Bot",176,40,true);practice:SetPoint("TOPLEFT",18,-164);practice:SetScript("OnClick",function()local ok,msg=BG.StartPractice(selectedGame,gs.difficulty);if not ok then ShowStatus(msg or "Could not start practice.")end end)
    local diffLabel=DText(play,"BOT DIFFICULTY",8,p.muted,FONT);diffLabel:SetPoint("TOPLEFT",222,-153)
    local dx=222;for _,entry in ipairs({{"easy","EASY"},{"normal","NORMAL"},{"hard","HARD"}}) do local id,label=entry[1],entry[2];local b=DBtn(play,label,88,28,gs.difficulty==id);b:SetPoint("TOPLEFT",dx,-171);dx=dx+94;b:SetScript("OnClick",function()BG.SetPracticeDifficulty(selectedGame,id);RPAL.RefreshBoardGame()end) end
    local aiText=DText(play,selectedGame=="chess" and "Easy explores random legal moves · Normal values material and the center · Hard also evaluates the opponent's strongest immediate reply." or "Easy moves casually · Normal prioritizes crowns and position · Hard evaluates the opponent's strongest immediate reply.",8.8,p.muted);aiText:SetPoint("TOPLEFT",518,-155);aiText:SetWidth(445);aiText:SetWordWrap(true)
    local note=DText(play,"Online games are authoritative on the challenger. Only compact board state and moves are sent through ODYN addon messages.",8,p.muted);note:SetPoint("BOTTOMLEFT",18,16)
end

local CHESS_ASSET_TYPE={K="king",Q="queen",R="rook",B="bishop",N="knight",P="pawn"}
local function BoardPieceTexture(game,pieceType,color)
    color=tostring(color or "white")
    if game=="chess" then
        local typ=CHESS_ASSET_TYPE[tostring(pieceType or "P"):upper()] or "pawn"
        return BOARD_ASSET.."chess_"..typ.."_"..color..".tga"
    end
    return BOARD_ASSET.."checker_"..color..".tga"
end
local function PreviewPiece(parent,game,pieceType,color,x,y,size)
    local t=parent:CreateTexture(nil,"ARTWORK");t:SetTexture(BoardPieceTexture(game,pieceType,color));t:SetSize(size,size);t:SetPoint("TOPLEFT",x,y);return t
end
local function PiecePreview(parent,setID,game)
    local set=BG.GetPieceSet(setID);local primary=BG.GetPieceVisual(setID,false);local alternate=BG.GetPieceVisual(setID,true)
    local host=Dynamic(CreateFrame("Frame",nil,parent,"BackdropTemplate"));host:SetSize(204,76);Backdrop(host,set.edge,Mul(set.base,.34),.98)
    local mainLabel=DText(host,"MAIN",7.4,P().muted,FONT);mainLabel:SetPoint("TOPLEFT",8,-7)
    local altLabel=DText(host,"AUTO CONTRAST",7.4,P().muted,FONT);altLabel:SetPoint("TOPLEFT",105,-7)
    if game=="chess" then
        for i,typ in ipairs({"K","Q","N"}) do PreviewPiece(host,game,typ,primary.color,7+(i-1)*30,-22,30) end
        for i,typ in ipairs({"K","Q","N"}) do PreviewPiece(host,game,typ,alternate.color,104+(i-1)*30,-22,30) end
    else
        for i=1,3 do PreviewPiece(host,game,"P",primary.color,7+(i-1)*30,-22,30) end
        for i=1,3 do PreviewPiece(host,game,"P",alternate.color,104+(i-1)*30,-22,30) end
    end
    return host
end

local function RenderCustomization(content)
    local gs=BG.GetGameState(selectedGame);local p=P();local h=DText(content,"CUSTOMIZATION",18,p.gold,TITLE_FONT);h:SetPoint("TOPLEFT",8,-7);local hint=DText(content,"Your selections belong to this game and are transmitted to the opponent when a match begins. Table themes color only your half of the board.",9,p.muted);hint:SetPoint("TOPLEFT",10,-36);hint:SetWidth(960);hint:SetWordWrap(true)
    local themePanel=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));themePanel:SetPoint("TOPLEFT",8,-72);themePanel:SetSize(1130,280);Backdrop(themePanel,p.border,p.panel,.98);DressPanel(themePanel,p.gold);local th=DText(themePanel,"TABLE HALF THEMES",10.2,p.gold,FONT);th:SetPoint("TOPLEFT",16,-14)
    for i,id in ipairs(BG.THEME_ORDER) do
        local def=BG.GetTheme(id);local alt=BG.GetThemeVisual(id,true);local col=(i-1)%3;local row=math.floor((i-1)/3);local card=Dynamic(CreateFrame("Button",nil,themePanel,"BackdropTemplate"));card:SetSize(350,94);card:SetPoint("TOPLEFT",18+col*365,-48-row*105);Backdrop(card,gs.theme==id and p.gold or p.border,def.rail,.99)
        local mainTag=DText(card,"MAIN",7.4,p.muted,FONT);mainTag:SetPoint("TOPLEFT",12,-34)
        local altTag=DText(card,"AUTO CONTRAST",7.4,p.muted,FONT);altTag:SetPoint("TOPLEFT",132,-34)
        for q=1,4 do local sq=card:CreateTexture(nil,"ARTWORK");sq:SetSize(24,24);sq:SetPoint("TOPLEFT",12+(q-1)*25,-49);local c=((q%2)==0) and def.light or def.dark;sq:SetColorTexture(c[1],c[2],c[3],1) end
        for q=1,4 do local sq=card:CreateTexture(nil,"ARTWORK");sq:SetSize(24,24);sq:SetPoint("TOPLEFT",132+(q-1)*25,-49);local c=((q%2)==0) and alt.light or alt.dark;sq:SetColorTexture(c[1],c[2],c[3],1) end
        local name=DText(card,def.name,9.2,p.pale,FONT);name:SetPoint("TOPLEFT",12,-10);local status=DText(card,gs.theme==id and "SELECTED" or "SELECT",7.5,gs.theme==id and p.gold or p.muted);status:SetPoint("BOTTOMRIGHT",-10,9);card:SetScript("OnClick",function()BG.SetTheme(selectedGame,id);RPAL.RefreshBoardGame()end)
    end
    local pieces=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));pieces:SetPoint("TOPLEFT",8,-366);pieces:SetPoint("BOTTOMRIGHT",-8,8);Backdrop(pieces,p.border,p.panel2,.98);DressPanel(pieces,p.accent);local ph=DText(pieces,"PIECE SETS",10.2,p.gold,FONT);ph:SetPoint("TOPLEFT",16,-14)
    for i,id in ipairs(BG.PIECE_SET_ORDER) do
        local col=(i-1)%3;local row=math.floor((i-1)/3);local holder=Dynamic(CreateFrame("Button",nil,pieces,"BackdropTemplate"));holder:SetSize(350,94);holder:SetPoint("TOPLEFT",18+col*365,-48-row*97);Backdrop(holder,gs.pieceSet==id and p.gold or p.border,Mul(p.main,.75),.99);local preview=PiecePreview(holder,id,selectedGame);preview:SetPoint("LEFT",10,0);local def=BG.GetPieceSet(id);local n=DText(holder,def.name,9.2,p.pale,FONT);n:SetPoint("TOPLEFT",226,-26);local st=DText(holder,gs.pieceSet==id and "SELECTED" or "SELECT",7.5,gs.pieceSet==id and p.gold or p.muted);st:SetPoint("TOPLEFT",226,-52);holder:SetScript("OnClick",function()BG.SetPieceSet(selectedGame,id);RPAL.RefreshBoardGame()end)
    end
end

local function RenderHistory(content)
    local p=P();local h=DText(content,"HISTORY",18,p.gold,TITLE_FONT);h:SetPoint("TOPLEFT",8,-7);local hist=BG.GetGameState(selectedGame).history or {};local meta=DText(content,#hist==0 and "No recorded games yet." or (tostring(#hist).." recent games"),9,p.muted);meta:SetPoint("TOPLEFT",10,-36)
    local list=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));list:SetPoint("TOPLEFT",8,-70);list:SetPoint("BOTTOMRIGHT",-8,8);Backdrop(list,p.border,p.panel,.98);DressPanel(list,p.gold)
    if #hist==0 then local e=DText(list,"Finish an online or bot match and it will appear here.",10,p.muted);e:SetPoint("CENTER",0,20);return end
    local y=-18
    for i,rec in ipairs(hist) do if i>14 then break end;local row=Dynamic(CreateFrame("Frame",nil,list,"BackdropTemplate"));row:SetSize(1080,38);row:SetPoint("TOPLEFT",18,y);Backdrop(row,p.border,(i%2==0) and p.panel2 or Mul(p.panel,1.05),.92);local outcome=DText(row,tostring(rec.outcome or "Game"),9.2,(rec.outcome=="Victory" and {0.52,1,.62,1}) or (rec.outcome=="Defeat" and {1,.48,.42,1}) or p.gold,FONT);outcome:SetPoint("LEFT",12,0);outcome:SetWidth(92);outcome:SetJustifyH("LEFT");local opp=DText(row,"vs  "..Short(rec.opponent,34),9,p.pale);opp:SetPoint("LEFT",outcome,"RIGHT",8,0);local mode=DText(row,string.upper(tostring(rec.mode or "online")),8,p.muted);mode:SetPoint("LEFT",438,0);local moves=DText(row,tostring(rec.moves or 0).." moves",8,p.muted);moves:SetPoint("LEFT",610,0);local dateText=(date and date("%d %b %Y  %H:%M",tonumber(rec.at) or 0)) or "";local dt=DText(row,dateText,8,p.muted);dt:SetPoint("RIGHT",-12,0);y=y-44 end
end

function RPAL.RefreshBoardGame()
    local f=EnsureMain();RestyleMain();Clear(f.content)
    if selectedTab=="customization" then RenderCustomization(f.content) elseif selectedTab=="history" then RenderHistory(f.content) else RenderPlay(f.content) end
end
function RPAL.OpenBoardGame(game,tab)
    if BG.GAMES[game] then selectedGame=game end;if tab=="play" or tab=="customization" or tab=="history" then selectedTab=tab end
    if matchFrame then matchFrame:Hide() end
    if RPAL.UI and RPAL.UI.main then RPAL.UI.main:Hide() end;RPAL.RefreshBoardGame();local f=EnsureMain();if RPAL.PrepareWindow then RPAL.PrepareWindow(f,nil,.15)else f:Show();f:Raise()end;return f
end

-- ============================================================
-- Match table
-- ============================================================
local function EnsureMatch()
    if matchFrame then return matchFrame end
    local f=CreateFrame("Frame","ODYN_BoardGameMatch",UIParent,"BackdropTemplate");matchFrame=f;f:SetSize(1240,790);f:SetPoint("CENTER");if RPAL.ApplyUILayer then RPAL.ApplyUILayer(f,"WORKSPACE")else f:SetFrameStrata("DIALOG");f:SetFrameLevel(360);f:SetToplevel(true)end;f:SetClampedToScreen(true);f:Hide();if RPAL.RegisterEscapeFrame then RPAL.RegisterEscapeFrame(f) end
    f:SetMovable(true);f:EnableMouse(true);f:RegisterForDrag("LeftButton");f:SetScript("OnDragStart",function(self)if IsAltKeyDown() then self:StartMoving() end end);f:SetScript("OnDragStop",function(self)self:StopMovingOrSizing()end);f:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=16,insets={left=4,right=4,top=4,bottom=4}})
    f.content=CreateFrame("Frame",nil,f);f.content:SetPoint("TOPLEFT",14,-14);f.content:SetPoint("BOTTOMRIGHT",-14,14)
    return f
end

local CHESS_NAMES={K="KING",Q="QUEEN",R="ROOK",B="BISHOP",N="KNIGHT",P="PAWN"}
local function PieceSide(game,p)
    if game=="chess" then if p=="." then return nil end;return p:match("%u") and 1 or 2 end
    if p=="x" or p=="X" then return 1 elseif p=="o" or p=="O" then return 2 end
end
local function IsKingChecker(p) return p=="X" or p=="O" end
local function PiecePalette(visual)
    visual=visual or {};return visual.base or {.8,.8,.8,1},visual.edge or {.3,.3,.3,1},visual.ink or {1,1,1,1}
end
local function DisplayToBoard(dr,dc,viewerSide)
    if viewerSide==2 then dr,dc=9-dr,9-dc end
    return (dr-1)*8+dc
end
local function BoardToDisplay(index,viewerSide)
    local r=math.floor((index-1)/8)+1;local c=((index-1)%8)+1;if viewerSide==2 then r,c=9-r,9-c end;return r,c
end

local function ShowMatchEndScreen(parent,m,viewerSide,mine,opp,p)
    if not m or not m.finished or m._endScreenDismissed then return end
    local overlay=Dynamic(CreateFrame("Frame",nil,parent,"BackdropTemplate"));overlay:SetAllPoints(parent);overlay:SetFrameLevel((parent:GetFrameLevel() or 1)+90);overlay:EnableMouse(true)
    local shade=overlay:CreateTexture(nil,"BACKGROUND");shade:SetAllPoints();shade:SetColorTexture(0,0,0,.68)

    local card=CreateFrame("Frame",nil,overlay,"BackdropTemplate");card:SetSize(548,356);card:SetPoint("CENTER",0,4);Backdrop(card,p.gold,Mul(p.panel,.94),.995);DressPanel(card,p.gold)
    local inner=card:CreateTexture(nil,"BACKGROUND",nil,2);inner:SetTexture(WHITE);inner:SetPoint("TOPLEFT",12,-12);inner:SetPoint("BOTTOMRIGHT",-12,12);inner:SetColorTexture(p.main[1],p.main[2],p.main[3],.55)

    local result,resultColor
    if m.winner==0 then
        result="DRAW";resultColor=p.gold
    elseif viewerSide==1 or viewerSide==2 then
        if m.winner==viewerSide then result="VICTORY";resultColor={.56,1,.67,1} else result="DEFEAT";resultColor={1,.49,.45,1} end
    else
        result="MATCH COMPLETE";resultColor=p.gold
    end

    local gameLabel=Text(card,string.upper((BG.GAMES[m.game] and BG.GAMES[m.game].name) or "GAME").." COMPLETE",8.6,p.muted,FONT);gameLabel:SetPoint("TOP",0,-34)
    local outcome=Text(card,result,27,resultColor,TITLE_FONT);outcome:SetPoint("TOP",gameLabel,"BOTTOM",0,-9)

    local winnerPlayer=(m.winner and m.winner~=0 and m.players and m.players[m.winner]) or nil
    local sentence
    if m.winner==0 then
        sentence="The match ended in a draw."
    elseif viewerSide==1 or viewerSide==2 then
        if m.winner==viewerSide then sentence="You defeated "..Short((opp and opp.display) or "your opponent",34).."." else sentence=Short((winnerPlayer and winnerPlayer.display) or (opp and opp.display) or "Your opponent",34).." won the match." end
    else
        sentence=Short((winnerPlayer and winnerPlayer.display) or "A player",34).." won the match."
    end
    local summary=Text(card,sentence,11,p.pale,FONT);summary:SetPoint("TOP",outcome,"BOTTOM",0,-12);summary:SetWidth(470);summary:SetJustifyH("CENTER");summary:SetWordWrap(true)

    local reasonLabel=Text(card,"FINISH",7.7,p.muted,FONT);reasonLabel:SetPoint("TOPLEFT",72,-174)
    local reason=Text(card,(m.finishReason and m.finishReason~="" and m.finishReason) or "Game over",10.2,p.pale,FONT);reason:SetPoint("TOPLEFT",72,-194);reason:SetWidth(170);reason:SetJustifyH("LEFT")
    local movesLabel=Text(card,"MOVES",7.7,p.muted,FONT);movesLabel:SetPoint("TOP",0,-174)
    local moves=Text(card,tostring(m.moveCount or 0),10.2,p.pale,FONT);moves:SetPoint("TOP",0,-194)
    local modeLabel=Text(card,"MODE",7.7,p.muted,FONT);modeLabel:SetPoint("TOPRIGHT",-72,-174)
    local modeName=(m.mode=="practice") and "Bot Practice" or "Online Match"
    local mode=Text(card,modeName,10.2,p.pale,FONT);mode:SetPoint("TOPRIGHT",-72,-194);mode:SetJustifyH("RIGHT")

    local line=card:CreateTexture(nil,"ARTWORK");line:SetTexture(WHITE);line:SetPoint("TOPLEFT",48,-231);line:SetPoint("TOPRIGHT",-48,-231);line:SetHeight(1);line:SetColorTexture(p.border[1],p.border[2],p.border[3],.8)

    local view=Btn(card,"View Board",132,34,false);view:SetPoint("BOTTOMLEFT",42,28);view:SetScript("OnClick",function()m._endScreenDismissed=true;overlay:Hide()end)
    local menu=Btn(card,"Back to Menu",132,34,false);menu:SetPoint("BOTTOMRIGHT",-42,28);menu:SetScript("OnClick",function()overlay:Hide();if matchFrame then matchFrame:Hide() end;RPAL.OpenBoardGame(m.game,"play")end)
    if m.mode=="practice" then
        local again=Btn(card,"Play Again",132,34,true);again:SetPoint("BOTTOM",0,28);again:SetScript("OnClick",function()
            overlay:Hide();if matchFrame then matchFrame:Hide() end
            local gs=BG.GetGameState(m.game);local ok,msg=BG.StartPractice(m.game,gs and gs.difficulty or "normal");if not ok and msg then ShowStatus(msg) end
        end)
    else
        view:ClearAllPoints();menu:ClearAllPoints();view:SetPoint("BOTTOM",-78,28);menu:SetPoint("BOTTOM",78,28)
    end

    overlay:SetAlpha(1);overlay:Show()
    if UIFrameFadeIn then pcall(UIFrameFadeIn,overlay,.22,0,1) end
end

local function RenderMatch(m,viewerSide)
    selectedGame=m.game or selectedGame
    local f=EnsureMatch();Clear(f.content);selectedSquare=nil;viewerSide=tonumber(viewerSide) or m.viewerSide or 1;m.viewerSide=viewerSide
    local p=P(m.game);f:SetBackdropColor(p.main[1]*.48,p.main[2]*.48,p.main[3]*.48,.999);f:SetBackdropBorderColor(p.gold[1],p.gold[2],p.gold[3],1)
    local title=DText(f.content,string.upper(BG.GAMES[m.game].name),22,p.gold,TITLE_FONT);title:SetPoint("TOPLEFT",20,-10);local sub=DText(f.content,"ODYN TWO-SIDED TABLE",8,p.muted);sub:SetPoint("TOPLEFT",23,-43)
    local close=DBtn(f.content,"×",34,30,false);close:SetPoint("TOPRIGHT",-6,-4);close.label:SetFont(TITLE_FONT,Scaled(18),"OUTLINE");close:SetScript("OnClick",function()f:Hide()end)

    local mine=(viewerSide==1 or viewerSide==2) and m.players[viewerSide] or nil;local opp=(viewerSide==1 and m.players[2]) or (viewerSide==2 and m.players[1]) or m.players[2]
    -- Cosmetics are resolved locally: if both players bring the same or a very
    -- similar palette, only the opponent is switched to that pack's paired
    -- contrast variant. Network payloads remain the selected pack IDs.
    local anchorSide=(viewerSide==1 or viewerSide==2) and viewerSide or 1
    local contrastSide=(anchorSide==1 and 2 or 1)
    local aPlayer=m.players[anchorSide] or {};local cPlayer=m.players[contrastSide] or {}
    local themeContrast=BG.ThemeNeedsContrast(aPlayer.theme,cPlayer.theme)
    local pieceContrast=BG.PieceNeedsContrast(aPlayer.pieceSet,cPlayer.pieceSet)
    local visualTheme,visualPiece={},{}
    for side=1,2 do
        local pl=m.players[side] or {}
        visualTheme[side]=BG.GetThemeVisual(pl.theme,side==contrastSide and themeContrast)
        visualPiece[side]=BG.GetPieceVisual(pl.pieceSet,side==contrastSide and pieceContrast)
    end
    local left=Dynamic(CreateFrame("Frame",nil,f.content,"BackdropTemplate"));left:SetSize(238,590);left:SetPoint("TOPLEFT",16,-82);Backdrop(left,p.border,p.panel,.98);DressPanel(left,p.gold)
    local lh=DText(left,"YOUR SIDE",8,p.gold,FONT);lh:SetPoint("TOPLEFT",16,-16);local ln=DText(left,mine and mine.display or "Spectator",15,p.pale,FONT);ln:SetPoint("TOPLEFT",16,-40);ln:SetWidth(204);local lt=BG.GetTheme(mine and mine.theme);local ls=BG.GetPieceSet(mine and mine.pieceSet);local lm=DText(left,(lt and lt.name or "—").."\n"..(ls and ls.name or "—"),8.5,p.muted);lm:SetPoint("TOPLEFT",16,-76)
    local turnText=DText(left,"",11,p.gold,FONT);turnText:SetPoint("TOPLEFT",16,-140);turnText:SetWidth(205);turnText:SetWordWrap(true)
    if m.finished then turnText:SetText(m.winner==0 and "DRAW" or ((m.winner==viewerSide) and "VICTORY" or "DEFEAT")) elseif m.turnSide==viewerSide then turnText:SetText("YOUR TURN") else turnText:SetText("OPPONENT'S TURN") end
    local help=DText(left,m.game=="chess" and "Select one of your pieces, then choose a highlighted legal square." or "Select a piece, then choose a highlighted diagonal square. Captures are mandatory.",8.5,p.muted);help:SetPoint("TOPLEFT",16,-182);help:SetWidth(202);help:SetWordWrap(true)
    local ff=DBtn(left,"Forfeit",112,30,false);ff:SetPoint("BOTTOMLEFT",16,18);ff:SetScript("OnClick",function()local ok,msg=BG.Forfeit();if not ok and msg then ShowStatus(msg) end end)
    local back=DBtn(left,"Back to Menu",112,30,false);back:SetPoint("LEFT",ff,"RIGHT",-1,0);back:SetScript("OnClick",function()f:Hide();RPAL.OpenBoardGame(m.game,"play")end)
    if m.finished then ff:Hide();back:ClearAllPoints();back:SetPoint("BOTTOMLEFT",16,18);back:SetWidth(206) end

    local right=Dynamic(CreateFrame("Frame",nil,f.content,"BackdropTemplate"));right:SetSize(238,590);right:SetPoint("TOPRIGHT",-16,-82);Backdrop(right,p.border,p.panel2,.98);DressPanel(right,p.accent)
    local rh=DText(right,"OPPONENT",8,p.gold,FONT);rh:SetPoint("TOPLEFT",16,-16);local rn=DText(right,opp and opp.display or "Opponent",15,p.pale,FONT);rn:SetPoint("TOPLEFT",16,-40);rn:SetWidth(204);local ot=BG.GetTheme(opp and opp.theme);local os=BG.GetPieceSet(opp and opp.pieceSet);local contrastNote=((themeContrast or pieceContrast) and "\n|cffd8bd76AUTO CONTRAST ACTIVE|r") or "";local rm=DText(right,(ot and ot.name or "—").."\n"..(os and os.name or "—")..contrastNote,8.2,p.muted);rm:SetPoint("TOPLEFT",16,-76);rm:SetWidth(204);rm:SetWordWrap(true)
    local rule=DText(right,m.game=="chess" and "CHESS RULESET\nCheck-safe legal movement\nCastling · Auto-queen promotion" or "CHECKERS RULESET\nMandatory captures\nChained jumps · Kings",8.4,p.muted);rule:SetPoint("TOPLEFT",16,-140);rule:SetWidth(202);rule:SetWordWrap(true)
    local moveInfo=DText(right,"MOVE  "..tostring(m.moveCount or 0),10,p.gold,FONT);moveInfo:SetPoint("BOTTOMLEFT",16,24)

    local boardHost=Dynamic(CreateFrame("Frame",nil,f.content,"BackdropTemplate"));boardHost:SetSize(586,586);boardHost:SetPoint("TOP",0,-80);Backdrop(boardHost,p.gold,Mul(p.main,.45),1)
    local cell=68;local boardSize=544;local ox=21;local oy=-21;local squares={};local legalTargets={}
    local function RefreshSelection()
        for _,sq in ipairs(squares) do
            local idx=sq.boardIndex;local rr=math.floor((idx-1)/8)+1;local cc=((idx-1)%8)+1;local owner=(rr<=4 and 2 or 1);local th=visualTheme[owner] or BG.GetThemeVisual(m.players[owner] and m.players[owner].theme,false);local base=((rr+cc)%2==0) and th.light or th.dark
            if idx==selectedSquare then sq.bg:SetColorTexture(p.gold[1],p.gold[2],p.gold[3],.76)
            elseif legalTargets[idx] then sq.bg:SetColorTexture(p.accent[1],p.accent[2],p.accent[3],.55)
            elseif m.lastMove and (idx==m.lastMove.from or idx==m.lastMove.to) then local mix=Mix(base,p.gold,.28);sq.bg:SetColorTexture(mix[1],mix[2],mix[3],1)
            else sq.bg:SetColorTexture(base[1],base[2],base[3],1) end
        end
    end
    local function Select(index)
        if m.finished or viewerSide==0 then return end
        local piece=m.board[index];local side=PieceSide(m.game,piece)
        if selectedSquare and legalTargets[index] then
            local from=selectedSquare;selectedSquare=nil;legalTargets={};local ok,err=BG.TryMove(from,index);if not ok and err then ShowStatus(err) end;return
        end
        selectedSquare=nil;legalTargets={}
        if side==viewerSide and m.turnSide==viewerSide then
            local moves=BG.GetLegalMoves(m,viewerSide,index);if #moves>0 then selectedSquare=index;for _,mv in ipairs(moves) do legalTargets[mv.to]=true end end
        end
        RefreshSelection()
    end
    for dr=1,8 do for dc=1,8 do
        local idx=DisplayToBoard(dr,dc,viewerSide);local br=math.floor((idx-1)/8)+1;local bc=((idx-1)%8)+1;local owner=(br<=4 and 2 or 1);local theme=visualTheme[owner] or BG.GetThemeVisual(m.players[owner] and m.players[owner].theme,false);local base=((br+bc)%2==0) and theme.light or theme.dark
        local sq=CreateFrame("Button",nil,boardHost);sq:SetSize(cell,cell);sq:SetPoint("TOPLEFT",ox+(dc-1)*cell,oy-(dr-1)*cell);sq.boardIndex=idx;sq.bg=sq:CreateTexture(nil,"BACKGROUND");sq.bg:SetAllPoints();sq.bg:SetColorTexture(base[1],base[2],base[3],1);squares[#squares+1]=sq
        local piece=m.board[idx];local side=PieceSide(m.game,piece)
        if side then
            local visual=visualPiece[side] or BG.GetPieceVisual(m.players[side] and m.players[side].pieceSet,false);local baseP,edgeP,inkP=PiecePalette(visual)
            local shadow=sq:CreateTexture(nil,"BORDER");shadow:SetTexture(WHITE);shadow:SetSize(m.game=="chess" and 38 or 42,m.game=="chess" and 12 or 14);shadow:SetPoint("CENTER",1,-20);shadow:SetColorTexture(0,0,0,.16)
            local art=sq:CreateTexture(nil,"ARTWORK");art:SetTexture(BoardPieceTexture(m.game,piece,visual.color));art:SetSize(m.game=="chess" and 62 or 54,m.game=="chess" and 62 or 54);art:SetPoint("CENTER",0,m.game=="chess" and 1 or 0)
            if m.game=="checkers" and IsKingChecker(piece) then
                local badge=CreateFrame("Frame",nil,sq,"BackdropTemplate");badge:SetSize(22,22);badge:SetPoint("CENTER",0,0);Backdrop(badge,edgeP,baseP,.88);local k=Text(badge,"K",10,inkP,FONT);k:SetPoint("CENTER",0,1)
            end
        end
        sq:SetScript("OnClick",function()Select(idx)end);sq:SetScript("OnEnter",function(self)if not selectedSquare then local piece=m.board[idx];if PieceSide(m.game,piece)==viewerSide and m.turnSide==viewerSide then self.bg:SetAlpha(.82) end end end);sq:SetScript("OnLeave",function(self)self.bg:SetAlpha(1)end)
    end end
    RefreshSelection()
    local topOwner=(viewerSide==1 and m.players[2]) or m.players[1];local bottomOwner=(viewerSide==1 and m.players[1]) or m.players[2]
    local topSide=(topOwner==m.players[1]) and 1 or 2;local bottomSide=(bottomOwner==m.players[1]) and 1 or 2
    local topTheme=visualTheme[topSide] or BG.GetThemeVisual(topOwner and topOwner.theme,false);local bottomTheme=visualTheme[bottomSide] or BG.GetThemeVisual(bottomOwner and bottomOwner.theme,false)
    local topRail=boardHost:CreateTexture(nil,"BORDER");topRail:SetTexture(WHITE);topRail:SetPoint("TOPLEFT",5,-5);topRail:SetPoint("TOPRIGHT",-5,-5);topRail:SetHeight(13);topRail:SetColorTexture(topTheme.rail[1],topTheme.rail[2],topTheme.rail[3],1)
    local botRail=boardHost:CreateTexture(nil,"BORDER");botRail:SetTexture(WHITE);botRail:SetPoint("BOTTOMLEFT",5,5);botRail:SetPoint("BOTTOMRIGHT",-5,5);botRail:SetHeight(13);botRail:SetColorTexture(bottomTheme.rail[1],bottomTheme.rail[2],bottomTheme.rail[3],1)
    local status=Dynamic(CreateFrame("Frame",nil,f.content,"BackdropTemplate"));status:SetSize(586,44);status:SetPoint("TOP",boardHost,"BOTTOM",0,-10);Backdrop(status,p.border,p.panel2,.98);local st=DText(status,"",9.2,p.pale,FONT);st:SetPoint("CENTER");if m.finished then st:SetText((m.finishReason~="" and m.finishReason or "Game over")..(m.winner==0 and "  ·  DRAW" or "")) elseif m.game=="checkers" and m.forcedFrom then st:SetText("CHAIN CAPTURE  ·  Continue with the highlighted piece") else st:SetText((m.turnSide==viewerSide and "YOUR MOVE" or "WAITING FOR OPPONENT").."  ·  "..tostring(m.moveCount or 0).." moves") end
    f:Show();f:Raise()
    if m.finished then ShowMatchEndScreen(f.content,m,viewerSide,mine,opp,p) end
end

function RPAL.OpenBoardGameMatch(m,viewerSide) if frame then frame:Hide() end;RenderMatch(m,viewerSide) end
function RPAL.RefreshBoardGameMatchUI(m,viewerSide) if matchFrame and matchFrame:IsShown() then RenderMatch(m,viewerSide) end end

-- ============================================================
-- Incoming challenge prompt
-- ============================================================
function RPAL.ShowBoardGameChallenge(id,ch)
    local previousGame=selectedGame
    selectedGame=ch.game or selectedGame
    if challengeFrame then challengeFrame:Hide();challengeFrame:SetParent(nil);challengeFrame=nil end
    local p=P(ch.game);local sh=CreateFrame("Frame",nil,UIParent);sh:SetAllPoints(UIParent);sh:SetFrameStrata("FULLSCREEN_DIALOG");sh:SetFrameLevel(9000);sh:EnableMouse(true);local shade=sh:CreateTexture(nil,"BACKGROUND");shade:SetAllPoints();shade:SetColorTexture(0,0,0,.55)
    local m=CreateFrame("Frame",nil,sh,"BackdropTemplate");m:SetSize(470,250);m:SetPoint("CENTER");Backdrop(m,p.gold,p.panel,.995);DressPanel(m,p.gold);challengeFrame=sh
    sh:HookScript("OnHide",function()selectedGame=previousGame;if challengeFrame==sh then challengeFrame=nil end end)
    local h=Text(m,string.upper(BG.GAMES[ch.game].name).." CHALLENGE",16,p.gold,TITLE_FONT);h:SetPoint("TOP",0,-24);local who=Text(m,Short(ch.display~="" and ch.display or ch.sender,38),12,p.pale,FONT);who:SetPoint("TOP",h,"BOTTOM",0,-16);local info=Text(m,"invited you to play. Their half of the table will use\n"..BG.GetTheme(ch.theme).name.." · "..BG.GetPieceSet(ch.pieceSet).name,9,p.muted);info:SetPoint("TOP",who,"BOTTOM",0,-12);info:SetJustifyH("CENTER")
    local decline=Btn(m,"Decline",110,30,false);decline:SetPoint("BOTTOMLEFT",72,24);decline:SetScript("OnClick",function()BG.DeclineChallenge(id);sh:Hide()end);local accept=Btn(m,"Accept",110,30,true);accept:SetPoint("BOTTOMRIGHT",-72,24);accept:SetScript("OnClick",function()local ok,msg=BG.AcceptChallenge(id);sh:Hide();if not ok and msg then ShowStatus(msg) end end)
    sh:Show()
end
