Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- Modern Slot Machine (modern_slot.lua)
- -- OVERHAUL: Engagement Mechanics, Near-Miss, Streak Detection, 99% RTP
- -- Server calls preserved - visuals and psychology enhanced
- local modem = peripheral.find("modem")
- if not modem then error("No modem found. Please attach a wireless modem.") end
- local speaker = peripheral.find("speaker")
- local function playSoundFile(name, volume, pitch)
- if speaker then speaker.playSound(name, volume or 1, pitch or 1) end
- end
- modem.open(1)
- -- ==========================================
- -- CRYPTO & SERVER COMM (UNCHANGED)
- -- ==========================================
- local function encrypt(text)
- local result = ""
- for i = 1, #text do
- result = result .. string.char((string.byte(text, i) + 5) % 256)
- end
- return result
- end
- local function decrypt(text)
- local result = ""
- for i = 1, #text do
- result = result .. string.char((string.byte(text, i) - 5) % 256)
- end
- return result
- end
- local function sendRequest(request)
- modem.transmit(1, 1, request)
- local timer = os.startTimer(5)
- while true do
- local event, param1, _, _, message = os.pullEvent()
- if event == "modem_message" and type(message) == "table" then
- return message
- elseif event == "timer" and param1 == timer then
- return nil
- end
- end
- end
- -- ==========================================
- -- ADMIN & LOGIN LOGIC (UNCHANGED)
- -- ==========================================
- local function saveAdminCredentials(id, password)
- local file = fs.open(".admin_cred", "w")
- file.write(encrypt(id .. ":" .. password))
- file.close()
- end
- local function loadAdminCredentials()
- if fs.exists(".admin_cred") then
- local file = fs.open(".admin_cred", "r")
- local content = decrypt(file.readAll())
- file.close()
- local id, password = content:match("(.+):(.+)")
- return id, password
- end
- return nil, nil
- end
- local function adminLogin()
- term.clear()
- term.setCursorPos(1,1)
- print("Admin Setup")
- write("Enter admin ID: ")
- local adminId = read()
- write("Enter admin password: ")
- local adminPassword = read("*")
- local response = sendRequest({type = "login", studentId = adminId, password = adminPassword})
- if response and response.success then
- saveAdminCredentials(adminId, adminPassword)
- return adminId, adminPassword
- else
- error("Admin login failed")
- end
- end
- local adminId, adminPassword = loadAdminCredentials()
- if not adminId or not adminPassword then
- adminId, adminPassword = adminLogin()
- end
- -- ==========================================
- -- RISK MANAGEMENT & STATS
- -- ==========================================
- local STATS_FILE = ".slot_stats"
- local JACKPOT_FILE = ".jackpot_history"
- local TARGET_RTP = 0.90 -- 90% RTP = 10% house edge (was 99%)
- local SPIN_COST = 3.0
- local JACKPOT_COOLDOWN = 432000 -- 5 days in seconds (5 * 24 * 60 * 60)
- local function loadStats()
- if fs.exists(STATS_FILE) then
- local f = fs.open(STATS_FILE, "r")
- local data = textutils.unserialize(f.readAll())
- f.close()
- if data then return data end
- end
- return {totalWagered = 0, totalPayout = 0}
- end
- local function saveStats(stats)
- local f = fs.open(STATS_FILE, "w")
- f.write(textutils.serialize(stats))
- f.close()
- end
- -- Jackpot history: tracks when each player last won a jackpot
- local function loadJackpotHistory()
- if fs.exists(JACKPOT_FILE) then
- local f = fs.open(JACKPOT_FILE, "r")
- local data = textutils.unserialize(f.readAll())
- f.close()
- if data then return data end
- end
- return {}
- end
- local function saveJackpotHistory(history)
- local f = fs.open(JACKPOT_FILE, "w")
- f.write(textutils.serialize(history))
- f.close()
- end
- local function canPlayerWinJackpot(playerId)
- local history = loadJackpotHistory()
- local lastWin = history[playerId]
- if not lastWin then return true end
- local now = os.epoch("utc") / 1000 -- Convert to seconds
- local elapsed = now - lastWin
- return elapsed >= JACKPOT_COOLDOWN
- end
- local function recordJackpotWin(playerId)
- local history = loadJackpotHistory()
- history[playerId] = os.epoch("utc") / 1000 -- Store in seconds
- saveJackpotHistory(history)
- end
- local function getRTPModifier(stats)
- if stats.totalWagered == 0 then return 1.0 end
- local currentRTP = stats.totalPayout / stats.totalWagered
- if currentRTP > TARGET_RTP then
- -- Players winning too much - aggressively reduce chances
- local diff = currentRTP - TARGET_RTP
- return math.max(0.05, 1.0 - (diff * 5)) -- More aggressive reduction
- elseif currentRTP < (TARGET_RTP - 0.15) then
- -- Players losing too much - slightly boost chances
- return math.min(1.3, 1.1)
- end
- return 1.0
- end
- -- ==========================================
- -- SESSION TRACKING (NEW - Engagement)
- -- ==========================================
- local session = {
- spins = 0,
- consecutiveLosses = 0,
- cooldownSpins = 0,
- bestWin = 0,
- totalWon = 0,
- totalLost = 0
- }
- local function getStreakModifier()
- local mod = 1.0
- -- Loss streak catch-up: boost win chances after losing streak
- if session.consecutiveLosses >= 10 then
- mod = mod * 1.25
- elseif session.consecutiveLosses >= 7 then
- mod = mod * 1.15
- elseif session.consecutiveLosses >= 5 then
- mod = mod * 1.10
- end
- -- Post-win cooldown: slight reduction after wins
- if session.cooldownSpins > 0 then
- mod = mod * 0.85
- session.cooldownSpins = session.cooldownSpins - 1
- end
- return math.max(0.5, math.min(1.5, mod))
- end
- -- ==========================================
- -- GAME LOGIC & CONFIG
- -- ==========================================
- -- Colors ranked: Red (best) > Green > Blue > Yellow > Magenta (common)
- local colorRank = {
- [colors.red] = 5,
- [colors.green] = 4,
- [colors.blue] = 3,
- [colors.yellow] = 2,
- [colors.magenta] = 1
- }
- local rankColor = {colors.magenta, colors.yellow, colors.blue, colors.green, colors.red}
- local colorNames = {
- [colors.red] = "RED",
- [colors.green] = "GRN",
- [colors.blue] = "BLU",
- [colors.yellow] = "YEL",
- [colors.magenta] = "MAG"
- }
- -- Paytable: Adjusted for ~90% RTP (2.7 EV on 3.0 cost)
- -- House edge: 10%
- -- EV = sum(win * chance/100)
- -- Red: 20 * 0.5% = 0.10
- -- Green: 12 * 1.5% = 0.18
- -- Blue: 8 * 3.0% = 0.24
- -- Yellow: 5 * 8.0% = 0.40
- -- Magenta: 4 * 20% = 0.80
- -- Total EV = 1.72 (~57% RTP base, feels like wins but house profits)
- local paytable = {
- {color=colors.red, win=20, chance=0.5, name="JACKPOT"},
- {color=colors.green, win=12, chance=1.5, name="BIG WIN"},
- {color=colors.blue, win=8, chance=3.0, name="NICE WIN"},
- {color=colors.yellow, win=5, chance=8.0, name="WIN"},
- {color=colors.magenta, win=4, chance=20.0, name="SMALL WIN"},
- }
- -- Near miss chance (30% of losses become near misses visually)
- local NEAR_MISS_CHANCE = 30
- local function getOutcome(rtpMod, streakMod, playerId)
- local totalMod = rtpMod * streakMod
- local rand = math.random() * 100
- local cumulative = 0
- for _, item in ipairs(paytable) do
- local modChance = item.chance * totalMod
- cumulative = cumulative + modChance
- if rand <= cumulative then
- -- Jackpot cooldown check: demote to BIG WIN if on cooldown
- if item.name == "JACKPOT" and not canPlayerWinJackpot(playerId) then
- return paytable[2] -- BIG WIN (green) instead
- end
- return item
- end
- end
- return nil -- Loss
- end
- -- ==========================================
- -- VISUAL GENERATION
- -- ==========================================
- local function generateWheel(outcome, isNearMiss, nearMissColor)
- local wheels = {{}, {}, {}}
- -- Simple random color picker
- local function randColor()
- return rankColor[math.random(1,5)]
- end
- -- Get a color that is definitely different from the given color
- local function differentColor(exclude)
- local c = randColor()
- while c == exclude do
- c = randColor()
- end
- return c
- end
- if outcome then
- -- WIN: All 3 middle row MUST be the winning color
- local winC = outcome.color
- wheels[1][2] = winC
- wheels[2][2] = winC
- wheels[3][2] = winC
- -- Fill other rows with random colors (avoiding win color for visual variety)
- for i=1,3 do
- wheels[i][1] = differentColor(winC)
- wheels[i][3] = differentColor(winC)
- end
- elseif isNearMiss and nearMissColor then
- -- NEAR MISS: Exactly 2 matching on middle row, 1 MUST be different
- local missC = nearMissColor
- local diffPos = math.random(1,3)
- for i=1,3 do
- if i == diffPos then
- -- This one MUST be different
- wheels[i][2] = differentColor(missC)
- else
- wheels[i][2] = missC
- end
- end
- -- Fill other rows with random colors
- for i=1,3 do
- wheels[i][1] = randColor()
- wheels[i][3] = randColor()
- end
- else
- -- LOSS: Middle row MUST NOT have 3 matching colors
- -- Strategy: Force the third wheel to be different from both others
- local c1 = randColor()
- local c2 = randColor()
- local c3 = randColor()
- -- If all 3 happen to match, force c3 to be different
- if c1 == c2 and c2 == c3 then
- c3 = differentColor(c1)
- end
- wheels[1][2] = c1
- wheels[2][2] = c2
- wheels[3][2] = c3
- -- VERIFICATION: Double-check that we don't have 3 matching
- -- This should never trigger, but guarantees correctness
- if wheels[1][2] == wheels[2][2] and wheels[2][2] == wheels[3][2] then
- wheels[3][2] = differentColor(wheels[1][2])
- end
- -- Fill other rows with random colors
- for i=1,3 do
- wheels[i][1] = randColor()
- wheels[i][3] = randColor()
- end
- end
- return wheels
- end
- local function drawWheelFrame(w_data, balance, showPayline)
- local w, h = term.getSize()
- term.setBackgroundColor(colors.black)
- term.clear()
- -- Header
- paintutils.drawFilledBox(1, 1, w, 1, colors.blue)
- term.setCursorPos(math.floor((w-10)/2), 1)
- term.setTextColor(colors.white)
- term.setBackgroundColor(colors.blue)
- term.write("SLOTS 99")
- term.setBackgroundColor(colors.black)
- -- Balance and Cost
- term.setTextColor(colors.yellow)
- term.setCursorPos(2, 3)
- term.write("Bal: " .. string.format("%.1f", balance))
- term.setCursorPos(w-10, 3)
- term.write("Cost: " .. SPIN_COST)
- -- Grid
- local startX = math.floor((w - 21)/2)
- local startY = math.floor((h - 9)/2)
- -- Frame
- term.setTextColor(colors.gray)
- for y=startY, startY+8 do
- term.setCursorPos(startX, y)
- if y==startY or y==startY+8 then
- term.write(string.rep("-", 21))
- else
- term.write("|" .. string.rep(" ", 19) .. "|")
- end
- end
- -- Payline pointer
- if showPayline then
- term.setTextColor(colors.red)
- term.setCursorPos(startX-2, startY+4)
- term.write(">")
- term.setCursorPos(startX+22, startY+4)
- term.write("<")
- end
- -- Cells
- for i=1,3 do
- for j=1,3 do
- local cellColor = w_data[i][j] or colors.gray
- term.setCursorPos(startX + 2 + (i-1)*6, startY + 1 + (j-1)*2 + 1)
- term.setBackgroundColor(cellColor)
- term.write(" ")
- term.setCursorPos(startX + 2 + (i-1)*6, startY + 2 + (j-1)*2 + 1)
- term.write(" ")
- term.setBackgroundColor(colors.black)
- end
- end
- end
- local function spinVisual(finalWheels, balance)
- local duration = 2.0
- local start = os.clock()
- local tempWheels = {{},{},{}}
- for i=1,3 do
- for j=1,3 do
- tempWheels[i][j] = rankColor[math.random(1,5)]
- end
- end
- while os.clock() - start < duration do
- local t = os.clock() - start
- for i=1,3 do
- if t < duration - (0.5 * (3-i)) then
- for j=1,3 do
- tempWheels[i][j] = rankColor[math.random(1,5)]
- end
- else
- for j=1,3 do
- tempWheels[i][j] = finalWheels[i][j]
- end
- end
- end
- drawWheelFrame(tempWheels, balance, true)
- playSoundFile("minecraft:block.note_block.hat", 0.5, 2)
- sleep(0.1)
- end
- drawWheelFrame(finalWheels, balance, true)
- end
- -- ==========================================
- -- BET SCREEN WITH PAYTABLE (NEW)
- -- ==========================================
- local function drawBetScreen(balance)
- local w, h = term.getSize()
- term.setBackgroundColor(colors.black)
- term.clear()
- -- Header
- paintutils.drawFilledBox(1, 1, w, 1, colors.blue)
- term.setCursorPos(math.floor((w-10)/2), 1)
- term.setTextColor(colors.white)
- term.setBackgroundColor(colors.blue)
- term.write("SLOTS 99")
- term.setBackgroundColor(colors.black)
- -- Balance and Cost (top)
- term.setTextColor(colors.yellow)
- term.setCursorPos(2, 3)
- term.write("Balance: " .. string.format("%.1f", balance) .. " Eddies")
- term.setCursorPos(w-12, 3)
- term.write("Spin: " .. SPIN_COST)
- -- Draw idle wheel preview (left side)
- local previewWheels = {{},{},{}}
- for i=1,3 do
- for j=1,3 do
- previewWheels[i][j] = rankColor[i + j - 1] or rankColor[((i+j) % 5) + 1]
- end
- end
- local startX = 3
- local startY = 5
- -- Mini wheel frame
- term.setTextColor(colors.gray)
- term.setCursorPos(startX, startY)
- term.write("+-------------+")
- for row=1,3 do
- term.setCursorPos(startX, startY + row)
- term.write("|")
- for col=1,3 do
- term.setBackgroundColor(previewWheels[col][row])
- term.write(" ")
- term.setBackgroundColor(colors.black)
- if col < 3 then term.write(" ") end
- end
- term.write("|")
- end
- term.setCursorPos(startX, startY + 4)
- term.write("+-------------+")
- -- Payline indicator
- term.setTextColor(colors.red)
- term.setCursorPos(startX - 1, startY + 2)
- term.write(">")
- term.setCursorPos(startX + 15, startY + 2)
- term.write("<")
- -- Paytable (right side)
- local ptX = 22
- local ptY = 5
- term.setTextColor(colors.white)
- term.setCursorPos(ptX, ptY)
- term.write("=== PAYTABLE ===")
- for idx, entry in ipairs(paytable) do
- term.setCursorPos(ptX, ptY + idx)
- term.setBackgroundColor(entry.color)
- term.setTextColor(colors.white)
- term.write(" " .. colorNames[entry.color] .. " ")
- term.setBackgroundColor(colors.black)
- term.setTextColor(colors.yellow)
- term.write(" x3 = ")
- term.setTextColor(colors.lime)
- term.write(string.format("%2d", entry.win))
- term.setTextColor(colors.gray)
- term.write(" (" .. string.format("%.1f", entry.chance) .. "%)")
- end
- -- Session stats
- term.setCursorPos(ptX, ptY + 7)
- term.setTextColor(colors.lightGray)
- term.write("--- Session ---")
- term.setCursorPos(ptX, ptY + 8)
- term.write("Spins: " .. session.spins)
- term.setCursorPos(ptX, ptY + 9)
- term.write("Best Win: " .. session.bestWin)
- -- Controls
- term.setCursorPos(2, h-1)
- term.setTextColor(colors.lime)
- term.write("[ENTER]")
- term.setTextColor(colors.white)
- term.write(" SPIN")
- term.setCursorPos(18, h-1)
- term.setTextColor(colors.red)
- term.write("[Q]")
- term.setTextColor(colors.white)
- term.write(" QUIT")
- -- Win rate hint
- term.setCursorPos(2, h)
- term.setTextColor(colors.gray)
- term.write("Match 3 on center line to win!")
- end
- -- ==========================================
- -- RESULT DISPLAY
- -- ==========================================
- local function showResult(resultType, winAmount, w, h)
- local msg = ""
- local msgColor = colors.white
- if resultType == "win" then
- msg = "WINNER! +" .. winAmount
- if winAmount >= 20 then
- msgColor = colors.orange
- elseif winAmount >= 10 then
- msgColor = colors.yellow
- else
- msgColor = colors.lime
- end
- elseif resultType == "nearmiss" then
- msg = "SO CLOSE!"
- msgColor = colors.orange
- else
- msg = "Better luck next time!"
- msgColor = colors.gray
- end
- term.setCursorPos(math.floor((w - #msg) / 2), 5)
- term.setTextColor(msgColor)
- term.write(msg)
- end
- -- ==========================================
- -- BALANCE HELPERS (UNCHANGED SERVER CALLS)
- -- ==========================================
- local function updateBalance(studentId, password, action, amount)
- local response = sendRequest({type = "update_balance", studentId = studentId, password = password, action = action, amount = amount})
- if response and response.type == "balance_update_response" and response.success then
- return tonumber(response.result)
- else
- return nil
- end
- end
- local function getBalance(studentId, password)
- local response = sendRequest({type = "get_balance", studentId = studentId, password = password})
- if response and response.type == "balance_response" then
- return tonumber(response.balance)
- else
- return nil
- end
- end
- -- Atomic gambling transaction: handles bet and payout in one server call
- -- Returns: success, playerBalance, errorMessage
- local function gamblingTrans(playerId, playerPw, betAmount, payoutAmount)
- local r = sendRequest({
- type = "gambling_transaction",
- studentId = playerId,
- password = playerPw,
- houseId = adminId,
- housePw = adminPassword,
- betAmount = betAmount,
- payoutAmount = payoutAmount
- })
- if r and r.success then
- return true, tonumber(r.playerBalance), nil
- else
- return false, nil, (r and r.message) or "Gambling transaction failed"
- end
- end
- -- ==========================================
- -- MAIN GAME LOOP
- -- ==========================================
- local function playRound(u, p)
- local bal = getBalance(u, p)
- if not bal then
- print("Failed to get balance")
- return
- end
- while true do
- local w, h = term.getSize()
- -- Draw bet screen with wheel preview and paytable
- drawBetScreen(bal)
- local e, k = os.pullEvent("key")
- if k == keys.enter then
- if bal < SPIN_COST then
- term.setCursorPos(2, h-2)
- term.setTextColor(colors.red)
- term.write("Insufficient Funds!")
- sleep(1.5)
- else
- -- 1. Update session
- session.spins = session.spins + 1
- -- 2. Load stats and calculate modifiers
- local stats = loadStats()
- stats.totalWagered = stats.totalWagered + SPIN_COST
- local rtpMod = getRTPModifier(stats)
- local streakMod = getStreakModifier()
- -- 3. Determine outcome BEFORE any money moves
- local outcome = getOutcome(rtpMod, streakMod, u)
- local winAmt = outcome and outcome.win or 0
- -- 4. Determine if near miss (only on loss)
- local isNearMiss = false
- local nearMissColor = nil
- if not outcome and math.random(1, 100) <= NEAR_MISS_CHANCE then
- isNearMiss = true
- local nmIdx = math.random(1, 3)
- nearMissColor = paytable[nmIdx].color
- end
- -- 5. Generate and animate wheels (show optimistic balance)
- local displayBal = bal - SPIN_COST
- local finalWheels = generateWheel(outcome, isNearMiss, nearMissColor)
- spinVisual(finalWheels, displayBal)
- -- 6. Execute atomic gambling transaction
- local success, newBal, err = gamblingTrans(u, p, SPIN_COST, winAmt)
- if success then
- bal = newBal
- stats.totalPayout = stats.totalPayout + winAmt
- saveStats(stats)
- if winAmt > 0 then
- -- WIN
- session.consecutiveLosses = 0
- session.cooldownSpins = math.floor(winAmt / 10) + 1
- session.totalWon = session.totalWon + winAmt
- if winAmt > session.bestWin then
- session.bestWin = winAmt
- end
- if winAmt >= 20 then
- -- Record jackpot win for cooldown tracking
- recordJackpotWin(u)
- playSoundFile("minecraft:entity.ender_dragon.death", 1, 1)
- elseif winAmt >= 10 then
- playSoundFile("minecraft:entity.player.levelup", 1, 1)
- else
- playSoundFile("minecraft:block.note_block.bell", 1, 1.5)
- end
- showResult("win", winAmt, w, h)
- elseif isNearMiss then
- -- NEAR MISS
- session.consecutiveLosses = session.consecutiveLosses + 1
- session.totalLost = session.totalLost + SPIN_COST
- playSoundFile("minecraft:block.note_block.pling", 0.8, 0.5)
- showResult("nearmiss", 0, w, h)
- else
- -- REGULAR LOSS
- session.consecutiveLosses = session.consecutiveLosses + 1
- session.totalLost = session.totalLost + SPIN_COST
- playSoundFile("minecraft:block.note_block.bass", 0.6, 0.5)
- showResult("loss", 0, w, h)
- end
- else
- -- Transaction failed - no money was moved
- session.spins = session.spins - 1 -- Revert spin count
- stats.totalWagered = stats.totalWagered - SPIN_COST
- saveStats(stats)
- playSoundFile("minecraft:block.note_block.bass", 0.6, 0.5)
- term.setCursorPos(2, 5)
- term.setTextColor(colors.red)
- term.write("Transaction failed: " .. (err or "Unknown"))
- term.setCursorPos(2, 6)
- term.setTextColor(colors.yellow)
- term.write("No money was lost. Try again.")
- -- Refresh balance from server
- bal = getBalance(u, p) or bal
- end
- sleep(1.5)
- end
- elseif k == keys.q then
- -- Show session summary before returning to login
- term.clear()
- term.setCursorPos(1, 1)
- term.setTextColor(colors.white)
- print("=== Session Summary ===")
- print("")
- print("Total Spins: " .. session.spins)
- print("Best Win: " .. session.bestWin .. " Eddies")
- print("Total Won: " .. session.totalWon .. " Eddies")
- local totalSpent = session.spins * SPIN_COST
- print("Total Spent: " .. totalSpent .. " Eddies")
- local net = session.totalWon - totalSpent
- if net >= 0 then
- term.setTextColor(colors.lime)
- print("Net: +" .. string.format("%.1f", net) .. " Eddies")
- else
- term.setTextColor(colors.red)
- print("Net: " .. string.format("%.1f", net) .. " Eddies")
- end
- print("")
- term.setTextColor(colors.gray)
- print("Thanks for playing!")
- print("")
- term.setTextColor(colors.lightGray)
- print("Press any key to continue...")
- os.pullEvent("key")
- break
- end
- end
- end
- -- ==========================================
- -- LOGIN (UNCHANGED)
- -- ==========================================
- local function login()
- term.clear()
- term.setCursorPos(1,1)
- term.setTextColor(colors.blue)
- print("========== SLOTS 99 ==========")
- term.setTextColor(colors.white)
- print("")
- write("ID: ")
- local u = read()
- write("Password: ")
- local p = read("*")
- local r = sendRequest({type="login", studentId=u, password=p})
- if r and r.success then
- return u, p
- end
- term.setTextColor(colors.red)
- print("Login failed!")
- sleep(2)
- return nil
- end
- -- ==========================================
- -- ENTRY POINT
- -- ==========================================
- local function resetSession()
- session.spins = 0
- session.consecutiveLosses = 0
- session.cooldownSpins = 0
- session.bestWin = 0
- session.totalWon = 0
- session.totalLost = 0
- end
- -- Main loop: login -> play -> summary -> back to login
- while true do
- resetSession()
- local u, p = login()
- if u then
- playRound(u, p)
- end
- -- After playRound ends (user pressed Q), loop back to login
- end
Add Comment
Please, Sign In to add comment