login

local arco = require "arco"

local cats = {
    "full-house", "four-of-a-kind", "little-straight", "big-straight",
    "choice", "yacht", "1s", "2s", "3s", "4s", "5s", "6s"
}

local function cat_index(cat)
    for i, c in ipairs(cats) do
        if c == cat then
            return i
        end
    end
end

local function calc_total(sheet)
    local total = 0
    for _, cat in ipairs(cats) do
        total = total + (sheet[cat] or 0)
    end
    return total
end

local function calc_data(dice)
    local sum = 0
    local hist = {0, 0, 0, 0, 0, 0}
    for _, die in ipairs(dice) do
        sum = sum + die
        hist[die] = hist[die] + 1
    end
    local min_count, max_count = math.huge, -math.huge
    for i = 1, 6 do
        local count = hist[i]
        if count > 0 then
            if count < min_count then
                min_count = count
            end
            if count > max_count then
                max_count = count
            end
        end
    end
    return sum, hist, min_count, max_count
end

local function calc_points(dice, cat)
    local sum, hist, min_count, max_count = calc_data(dice)
    local points = 0
    if cat == "full-house" then
        if min_count == 2 and max_count == 3 then
            points = sum
        end
    elseif cat == "four-of-a-kind" then
        if max_count >= 4 then
            for i = 1, 6 do
                if hist[i] >= 4 then
                    points = 4 * i
                    break
                end
            end
        end
    elseif cat == "little-straight" then
        if max_count == 1 and hist[6] == 0 then
            points = 30
        end
    elseif cat == "big-straight" then
        if max_count == 1 and hist[1] == 0 then
            points = 30
        end
    elseif cat == "choice" then
        points = sum
    elseif cat == "yacht" then
        if max_count == 5 then
            points = 50
        end
    else
        local num = tonumber(cat:sub(1, 1))
        points = num * hist[num]
    end
    return points
end

local Yacht = {}
Yacht.__index = Yacht

function Yacht:init()
    self.term:setup()
    self.term:enter_altbuf()
    self.term:hide_cursor()
end

function Yacht:quit()
    self.term:show_cursor()
    self.term:exit_altbuf()
    self.term:restore()
end

function Yacht:setup_game()
    self.term:clear()
    self.nh = tonumber(self.term:input("number of human players: "))
    self.nc = tonumber(self.term:input("number of computer players: "))
    self.np = self.nh + self.nc
    self.players = {}
    for i = 1, self.nh do
        table.insert(self.players, "H-"..i)
    end
    for i = 1, self.nc do
        table.insert(self.players, "C-"..i)
    end
    self.prng:shuffle(self.players)
    self.sheets = {}
    for i = 1, self.np do
        table.insert(self.sheets, {})
    end
end

function Yacht:run_game()
    for i = 1, 12 do
        self:run_round()
    end
    self:show_score()
end

function Yacht:run_round()
    self.term:clear()
    for i, p in ipairs(self.players) do
        self.pi = i
        self:show_sheet()
        local sheet = self.sheets[i]
        local is_bot = p:sub(1, 1) == "C"
        local dice = self:do_rolls(is_bot)
        local cat
        if is_bot then
            cat = self.bot:pick(sheet, dice)
            assert(sheet[cat] == nil and cat_index(cat) ~= nil)
            self.term:delay(1)
            for cur, curcat in ipairs(cats) do
                if sheet[curcat] == nil then
                    self:putsel(cur)
                    self.term:delay(1)
                    self:remsel(cur)
                    if curcat == cat then
                        break
                    end
                end
            end
            self.term:delay(2)
        else
            cat = cats[self:pick()]
        end
        local points = calc_points(dice, cat)
        sheet[cat] = points
        self:putpoints(cat_index(cat), points)
        self.term:goto(2+7*(i-1), 2)
        io.write(("%3d"):format(calc_total(sheet)))
        self.term:get_key()
        self.term:goto(1, 1)
    end
end

function Yacht:show_sheet()
    local sheet = self.sheets[self.pi]
    local fmt
    for i, p in ipairs(self.players) do
        if i == self.pi then
            fmt = "[%s]  "
        else
            fmt = " %s   "
        end
        io.write(fmt:format(p))
    end
    self.term:goto(1, 2)
    for i, p in ipairs(self.players) do
        if i == self.pi then
            fmt = "[%3d]  "
        else
            fmt = " %3d   "
        end
        io.write(fmt:format(calc_total(self.sheets[i])))
    end
    self.term:goto(1, 8)
    for i = 1, #cats/2 do
        local cat_l = cats[i]
        local cat_r = cats[i+6]
        local val_l = sheet[cat_l] or "--"
        if val_l == 0 then
            val_l = "XX"
        end
        local val_r = sheet[cat_r] or "--"
        if val_r == 0 then
            val_r = "XX"
        end
        local fmt = "%16s: %2s"..(" "):rep(4).."%8s: %2s"
        print(fmt:format(cat_l, val_l, cat_r, val_r))
    end
    self.term:goto(1, 4)
end

function Yacht:show_score()
    self.term:clear()
    self.term:down(3)
    print("== final score ==\n")
    for i, p in ipairs(self.players) do
        print(("%s: %d"):format(p, calc_total(self.sheets[i])))
    end
    print("\n=================")
    self.term:get_key()
end

function Yacht:do_rolls(is_bot)
    local pad = (" "):rep(15)
    print(pad.."roll: 1")
    self.term:clear_line()
    self.term.down()
    local dice, sel = {}, {}
    io.write((" "):rep(12).."[ - - - - - ]")
    self:roll(dice)
    for i = 2, 3 do
        if is_bot then
            local sheet = self.sheets[self.pi]
            sel = self.bot:select(sheet, dice, i-1)
            self.term:delay(3)
            self.term:goto_col(15)
            for cur = 1, 5 do
                io.write("^")
                self.term:left()
                self.term:delay(1)
                self.term:up(2)
                io.write(sel[cur] and "= " or "  ")
                self.term:delay(1)
                self.term:down(2)
                self.term:clear_line()
            end
            self.term:delay(3)
            self.term:up(3)
            self.term:goto_col(1)
        else
            self:select(sel)
        end
        for j = 1, 5 do
            if not sel[j] then
                dice[j] = nil
            end
        end
        print(pad.."roll: "..i.."\n")
        self:roll(dice)
    end
    self.term:up(2)
    self.term:clear_line()
    return dice
end

function Yacht:roll(dice)
    self.term:goto_col(15)
    for i = 1, 5 do
        if dice[i] == nil then
            io.write("- ")
        else
            self.term:right(2)
        end
    end
    self.term:goto_col(15)
    for i = 1, 5 do
        if dice[i] == nil then
            dice[i] = self.prng:randint(1, 6)
            self.term:delay(1)
            io.write(("%d "):format(dice[i]))
        else
            self.term:right(2)
        end
    end
    self.term:down()
end

function Yacht:select(sel)
    local cur = 1
    for i = 1, 5 do
        if not sel[i] then
            cur = i
            break
        end
    end
    self.term:goto_col(cur * 2 + 13)
    io.write("^")
    while true do
        local key = self.term:get_key()
        local update = true
        if key == "\n" then
            break
        elseif key == "a" or key == "h" then
            cur = (cur > 1) and (cur - 1) or 5
        elseif key == "d" or key == "l" then
            cur = (cur < 5) and (cur + 1) or 1
        elseif key == " " then
            sel[cur] = not sel[cur]
        else
            update = false
        end
        if update then
            self.term:up(2)
            self.term:clear_line()
            self.term:goto_col(15)
            for i = 1, 5 do
                io.write(sel[i] and "= " or "  ")
            end
            self.term:down(2)
            self.term:clear_line()
            self.term:goto_col(cur * 2 + 13)
            io.write("^")
        end
    end
    self.term:clear_line()
    self.term:up(3)
    self.term:goto_col(1)
end

function Yacht:getpos(s)
    if s <= #cats/2 then
        return 18, s+7
    else
        return 34, s+1
    end
end

function Yacht:putsel(s)
    self.term:goto(self:getpos(s))
    io.write("[")
    self.term:right(2)
    io.write("]")
end

function Yacht:remsel(s)
    self.term:goto(self:getpos(s))
    io.write(" -- ")
end

function Yacht:putpoints(s, p)
    self.term:goto(self:getpos(s))
    if p == 0 then
        io.write(" XX ")
    else
        io.write((" %2d "):format(p))
    end
end

function Yacht:pick()
    local sheet = self.sheets[self.pi]
    local sel
    for i, cat in ipairs(cats) do
        if sheet[cat] == nil then
            sel = i
            break
        end
    end
    self:putsel(sel)
    while true do
        local key = self.term:get_key()
        local update = true
        local newsel = sel
        if key == "\n" or key == " " then
            break
        elseif key == "w" or key == "k" then
            repeat
                newsel = (newsel > 1) and newsel - 1 or #cats
            until sheet[cats[newsel]] == nil
        elseif key == "s" or key == "j" then
            repeat
                newsel = (newsel < #cats) and newsel + 1 or 1
            until sheet[cats[newsel]] == nil
        elseif key == "a" or key == "d" or key == "h" or key == "l" then
            newsel = (sel <= #cats/2) and sel + 6 or sel - 6
            if sheet[cats[newsel]] ~= nil then
                update = false
            end
        else
            update = false
        end
        if update then
            self:remsel(sel)
            sel = newsel
            self:putsel(sel)
        end
    end
    return sel
end

local function new_yacht(Bot)
    local term = arco.tui.new_term()
    local prng = arco.rand.new_prng()
    local bot = setmetatable({prng=prng}, Bot)
    return setmetatable({term=term, prng=prng, bot=bot}, Yacht)
end

local Bob = {}
Bob.__index = Bob

local function log(msg)
    --~ io.write("\x1B7")       -- save cursor position
    --~ io.write("\x1B[15;1H")  -- goto row 15, col 1
    --~ io.write("\x1B[2K")     -- clear line
    --~ io.write(msg)           -- print msg
    --~ io.write("\x1B8")       -- restore cursor position
end

function Bob:select(sheet, dice, roll)
    local sum, hist, min_count, max_count = calc_data(dice)
    local round = 1
    for _, cat in ipairs(cats) do
        if sheet[cat] ~= nil then
            round = round + 1
        end
    end
    local min_fh, min_st, min_yt, min_rp, min_4k
    if round <= 4 then
        min_fh, min_st, min_yt, min_rp, min_4k = 3, 4, 4, 3, 3
    elseif round <= 8 then
        min_fh, min_st, min_yt, min_rp, min_4k = 2, 3, 3, 2, 2
    else  -- round > 8
        min_fh, min_st, min_yt, min_rp, min_4k = 1, 3, 3, 1, 1
    end
    local sel = {}
    if sheet["full-house"] == nil then
        local rep = {}
        for i = min_fh, 6 do
            if hist[i] >= 2 then
                table.insert(rep, i)
            end
        end
        if #rep == 2 then
            for i = 1, 5 do
                sel[i] = hist[dice[i]] >= 2
            end
            log("full-house")
            return sel
        end
    end
    if sheet["little-straight"] == nil or sheet["big-straight"] == nil then
        local little_match, big_match
        little_match = (hist[1] > 0) and 1 or 0
        big_match = (hist[6] > 0) and 1 or 0
        for i = 2, 5 do
            if hist[i] > 0 then
                little_match = little_match + 1
                big_match = big_match + 1
            end
        end
        local cat
        if sheet["little-straight"] ~= nil then
            if big_match >= min_st then
                cat = "big-straight"
            end
        elseif sheet["big-straight"] ~= nil then
            if little_match >= min_st then
                cat = "little-straight"
            end
        elseif math.max(big_match, little_match) >= min_st then
            if big_match > little_match then
                cat = "big-straight"
            else
                cat = "little-straight"
            end
        end
        local tosel
        if cat == "little-straight" then
            tosel = {1, 1, 1, 1, 1, 0}
        elseif cat == "big-straight" then
            tosel = {0, 1, 1, 1, 1, 1}
        end
        if cat ~= nil then
            for i = 1, 5 do
                local d = dice[i]
                if tosel[d] > 0 then
                    sel[i] = true
                    tosel[d] = tosel[d] - 1
                end
            end
            log(cat)
            return sel
        end
    end
    if sheet["yacht"] == nil and max_count >= min_yt then
        for i = 1, 5 do
            sel[i] = hist[dice[i]] == max_count
        end
        log("yacht")
        return sel
    end
    if max_count >= min_rp or sheet["choice"] ~= nil or sum < 12 then
        local d
        for i = 1, 6 do
            if hist[i] == max_count then
                d = i
            end
        end
        local cat = cats[d+6]
        if sheet[cat] == nil or (sheet["four-of-a-kind"] == nil and d >= min_4k) then
            for i = 1, 5 do
                sel[i] = dice[i] == d
            end
            log("repeat "..d)
            return sel
        end
    end
    if sheet["choice"] == nil then
        for i = 1, 5 do
            sel[i] = dice[i] > 3
        end
        log("choice")
        return sel
    end
    log("discard roll")
    return {}
end

function Bob:pick(sheet, dice, roll)
    local sum, hist, min_count, max_count = calc_data(dice)
    local good_cats = {"yacht", "four-of-a-kind", "full-house", "little-straight", "big-straight"}
    local good_mins = {1, 16, 17, 1, 1}
    for i, cat in ipairs(good_cats) do
        local thresh = good_mins[i]
        if sheet[cat] == nil and calc_points(dice, cat) >= thresh then
            log("good match: "..cat)
            return cat
        end
    end
    if sheet["choice"] == nil and sum >= 20 then
        log("ok match: choice")
        return "choice"
    end
    for i = 1, 6 do
        local cat = i.."s"
        if sheet[cat] == nil and hist[i] >= 2 then
            log("ok match: "..cat)
            return cat
        end
    end
    -- no good pick, discard a sane item
    local bad_cats = {"1s", "2s", "3s", "little-straight", "big-straight"}
    for _, cat in ipairs(bad_cats) do
        if sheet[cat] == nil then
            log("discard bad")
            return cat
        end
    end
    -- discard anything with points (useful on last few rounds)
    for _, cat in ipairs(bad_cats) do
        if sheet[cat] == nil  and calc_points(dice, cat) > 0 then
            log("discard with points")
            return cat
        end
    end
    -- no sane discard possible, discard anything
    log("insane discard")
    local cat
    repeat
        cat = cats[self.prng:randint(1, 12)]
    until sheet[cat] == nil
    return cat
end

local yacht = new_yacht(Bob)
yacht:init()
yacht:setup_game()
yacht:run_game()
yacht:quit()