To be able to write in the forum you need to authenticate. Meanwhile it's read-only.

[Resolved] Checker-Update needed for GC5C2AE

[Resolved] Checker-Update needed for GC5C2AE
April 18, 2026 09:40AM
Hello,

A while ago, I was kindly provided with this Challenge Checker: https://project-gc.com/Challenges/GC5C2AE/79495

If a cacher has a high number of finds (over 20k), the checker displays an error message. If the challenge hasn’t been completed, it would be helpful to see a message explaining why it wasn’t completed (caches not logged on the same day, missing characters, duplicate characters).

Many thanks in advance!

Regards,
ondraszch
Re: Checker-Update needed for GC5C2AE
April 18, 2026 12:50PM
I forwarded this request to the script author.
Re: Checker-Update needed for GC5C2AE
April 18, 2026 03:03PM
The checker has to check very large number of combination, when the cacher has many finds, the script may go to timeout without find the result.
As the conditions of this challenge are no more compatible with new rules for challenge cache (a challenge cannot be based on GC code), I have no interest to modify this script. Who gets the timeout, he should check the challenge manually.
Sorry, I cannot help you.
Re: Checker-Update needed for GC5C2AE
April 19, 2026 09:23AM
Hello,
Thank you very much for your quick response. I understand your reasoning for not modifying the script. So I ran the Lua script through Claude myself to analyze and optimize it. Here are the results:

-- ============================================================
-- Challenge Checker: "Sei deines eigenen Glückes Schmied"
-- GC5C2AE  –  optimized version
-- Changes vs. original:
--   * Bitmask arithmetic instead of string pattern matching
--   * Deduplication: one representative cache per unique bitmask
--   * Candidates sorted by popcount descending (greedy first)
--   * Forward-checking / pruning: abort branch early when
--     remaining candidates can no longer cover missing bits
--   * All original features preserved (inOneDay, oneHide,
--     twoInDay, hideAsJolly, excludeTypes)
-- ============================================================

local TIMEOUT = 2000000          -- raised; bitmask ops are much cheaper
local args    = {...}
local conf    = args[1].config
local profileName = args[1].profileName
local profileId   = args[1].profileId

PGC.print('Got profile name, ', profileName, "\n")

-- ---- Letter → bit-position mapping -------------------------
-- 31 valid GC-code characters (no I, L, O, S, U)
local LETTERS = {
  'A','B','C','D','E','F','G','H','J','K','M','N','P','Q','R',
  'T','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9'
}
local FULL_MASK = 0
local letter_bit = {}            -- char → bitmask (power of 2)
for i, ch in ipairs(LETTERS) do
  local b = 1 << (i - 1)        -- Lua 5.3+ bitwise
  letter_bit[ch] = b
  FULL_MASK = FULL_MASK | b
end
-- FULL_MASK == 2^31 - 1  (all 31 bits set)

-- ---- Helper: compute bitmask for a code string --------------
local function codeMask(code)
  local m = 0
  for i = 1, #code do
    local b = letter_bit[code:sub(i, i)]
    if b then m = m | b end
  end
  return m
end

-- ---- Helper: count set bits (popcount) ----------------------
local function popcount(n)
  local c = 0
  while n ~= 0 do
    n = n & (n - 1)
    c = c + 1
  end
  return c
end

-- ---- Helper: does a string contain duplicate characters? ----
local function hasDouble(str)
  local seen = {}
  for i = 1, #str do
    local ch = str:sub(i, i)
    if seen[ch] then return true end
    seen[ch] = true
  end
  return false
end

-- ---- Excluded types ----------------------------------------
local excludedTypes = {}
if conf.excludeTypes then
  for _, et in ipairs(conf.excludeTypes) do
    excludedTypes[et] = true
  end
end

-- ---- Fetch data --------------------------------------------
local finds = PGC.GetFinds(profileId, {
  fields = {'gccode', 'cache_name', 'visitdate', 'owner_id', 'type'},
  order  = 'OLDESTFIRST',
  filter = filter
})
local hides = PGC.GetHides(profileId, {fields = {'gccode', 'cache_name', 'hidden'}})
for _, h in ipairs(hides) do
  h.visitdate = "MY OWN"
end

if conf.oneHide then
  for _, h in ipairs(hides) do
    table.insert(finds, {
      gccode     = h.gccode,
      cache_name = h.cache_name,
      visitdate  = "MY OWN",
      owned      = true,
      owner_id   = profileId
    })
  end
end

PGC.print("finds: ", #finds, "\n")

-- ---- Pre-process: filter, attach mask ----------------------
local function preprocessList(list, useExcludeTypes)
  local result = {}
  for _, v in ipairs(list) do
    if useExcludeTypes and excludedTypes[v.type] then
      -- skip excluded type
    else
      local code = v.gccode:sub(3, -1)
      if not hasDouble(code) then
        v.code = code
        v.len  = #code
        v.mask = codeMask(code)
        -- mask must equal FULL_MASK restricted to its own bits,
        -- i.e. no letter appears twice → guaranteed by hasDouble check
        table.insert(result, v)
      end
    end
  end
  return result
end

local filtered_finds = preprocessList(finds, true)
local filtered_hides = preprocessList(hides, false)

PGC.print("not doubles: ", #filtered_finds, "\n")

-- ---- Deduplication by bitmask ------------------------------
-- Keep oldest (first) representative for each unique mask.
-- This is the key scalability improvement: 100 k finds collapse
-- to at most a few thousand distinct masks.
local function dedup(list)
  local seen = {}
  local result = {}
  for _, v in ipairs(list) do
    if not seen[v.mask] then
      seen[v.mask] = true
      table.insert(result, v)
    end
  end
  return result
end

-- ---- Special-condition check (unchanged logic) -------------
local function check_for_special_conditions(winner)
  if conf.oneHide then
    local countOwn = 0
    for _, v in ipairs(winner) do
      if v.owned then countOwn = countOwn + 1 end
    end
    if countOwn == 1 then return true end
    if countOwn > 1  then return false end
  end
  if conf.twoInDay then
    for i, v in ipairs(winner) do
      if not v.owned then
        for j = i + 1, #winner do
          local v2 = winner[j]
          if not v2.owned and v2.visitdate == v.visitdate then
            return true
          end
        end
      end
    end
  end
  if conf.twoInDay or conf.oneHide then return false end
  return true
end

-- ---- Global search state -----------------------------------
local winner   = {}
local odometer = 0
local curdate  -- used by hideAsJolly branch

-- ---- Precompute "union of all masks" prefix sums -----------
-- For a sorted candidate list we build a suffix-union array:
-- suffix_union = mask1 | mask2 | ... | mask[#list]  for j>=i
-- Used for forward-checking: if (curMask | suffix_union) ~= FULL_MASK
-- then no solution can be found from position i onward → prune.
local function buildSuffixUnion(list)
  local su = {}
  local acc = 0
  for i = #list, 1, -1 do
    acc = acc | list.mask
    su = acc
  end
  return su
end

-- ---- Core recursive search (bitmask edition) ---------------
local function add(list, suffix_union, n, curMask)
  -- Forward-check: can remaining candidates cover all missing bits?
  if n <= #list then
    local needed = FULL_MASK ~ curMask   -- bits still required  (XOR = difference)
    if (suffix_union[n] & needed) ~= needed then
      return false   -- impossible to complete → prune entire branch
    end
  end

  repeat
    -- Advance past candidates that overlap with curMask
    while n <= #list and (list[n].mask & curMask) ~= 0 do
      n = n + 1
    end

    -- Forward-check again after skipping
    if n > #list then
      -- hideAsJolly fallback: try a hide to fill the remaining gap
      if conf.hideAsJolly then
        local needed = FULL_MASK ~ curMask
        local pc = popcount(needed)
        if pc <= 6 then
          for _, h in ipairs(filtered_hides) do
            if  h.mask == needed
            and h.hidden < curdate
            then
              table.insert(winner, h)
              return true
            end
          end
        end
      end
      return false
    end

    -- Forward-check with remaining suffix
    local needed = FULL_MASK ~ curMask
    if (suffix_union[n] & needed) ~= needed then
      return false
    end

    local v       = list[n]
    local newMask = curMask | v.mask

    odometer = odometer + 1
    if odometer > TIMEOUT then return false end

    table.insert(winner, v)

    if newMask == FULL_MASK and check_for_special_conditions(winner) then
      PGC.print("GOT IT: ")
      return true
    end

    -- Build sub-list: only candidates compatible with newMask
    -- (no overlapping bits) from position n+1 onward
    local sublist = {}
    for i = n + 1, #list do
      if (list.mask & newMask) == 0 then
        table.insert(sublist, list)
      end
    end

    local sub_su = buildSuffixUnion(sublist)
    if add(sublist, sub_su, 1, newMask) then return true end

    winner[#winner] = nil
    n = n + 1
  until false
end

local function search(tab)
  -- Sort by popcount descending: try to cover most bits first
  table.sort(tab, function(a, b) return popcount(a.mask) > popcount(b.mask) end)
  -- Deduplicate
  tab = dedup(tab)
  PGC.print("deduplicated candidates: ", #tab, "\n")
  local su = buildSuffixUnion(tab)
  for i = 1, #tab do
    if add(tab, su, i, 0) then return true end
    if odometer > TIMEOUT then return false end
  end
  return false
end

-- ---- Main search -------------------------------------------
local ok = false

if conf.inOneDay then
  -- Group by visit date, sort within each day oldest-first
  local find_per_day = {}
  for _, v in ipairs(filtered_finds) do
    if not find_per_day[v.visitdate] then find_per_day[v.visitdate] = {} end
    table.insert(find_per_day[v.visitdate], v)
  end
  for d, tab in pairs(find_per_day) do
    curdate = d
    -- Need at least 6 caches on one day (min codes cover 6 chars each = 36 > 31)
    -- Original used >=6; keep that guard
    if #tab >= 6 then
      -- Sort by visitdate then mask-length inside day (original order preserved
      -- as tiebreaker; search() will re-sort by popcount)
      table.sort(tab, function(a, b)
        return a.visitdate < b.visitdate
            or (a.visitdate == b.visitdate and a.len < b.len)
      end)
      if search(tab) then ok = true; break end
    end
  end
else
  ok = search(filtered_finds)
end

-- ---- Output ------------------------------------------------
PGC.print("ok=", ok, "\n")
PGC.print("odometer=", odometer, "\n")

table.sort(winner, function(a, b) return a.visitdate < b.visitdate end)

local log  = {"I have found the following caches that fulfills the challenge:"}
local html = {"You have found the following caches that fulfills the challenge:<table>"}

for _, v in ipairs(winner) do
  PGC.print(v.code, "  ", v.visitdate, "  ", v.cache_name, "\n")
  table.insert(log,  v.visitdate .. "  " .. v.code .. "  " .. v.cache_name)
  table.insert(html, "<tr><td>" .. v.visitdate .. "</td><td>" .. v.code
                     .. "</td><td>" .. v.cache_name .. "</td></tr>\n")
end
table.insert(html, "</table>")

if not ok and odometer >= TIMEOUT then
  return {
    ok   = false,
    log  = false,
    html = "<h2>Attention, this can be a false negative as the script was not able to check all combinations.</h2>Please check it manually"
  }
end

log  = table.concat(log,  "\n")
html = table.concat(html)

if not ok then html = "" end
return { ok = ok, log = log, html = html }


The script uses the << and | operators (Lua 5.3+). Here is a fallback version that uses the bit library instead (which is available in LuaJIT and many 5.1 environments)

-- ============================================================
-- Challenge Checker: "Sei deines eigenen Glückes Schmied"
-- GC5C2AE  –  optimized version (Lua 5.1 / bit-library compatible)
-- Changes vs. original:
--   * Bitmask arithmetic instead of string pattern matching
--   * Deduplication: one representative cache per unique bitmask
--   * Candidates sorted by popcount descending (greedy first)
--   * Forward-checking / pruning: abort branch early when
--     remaining candidates can no longer cover missing bits
--   * All original features preserved (inOneDay, oneHide,
--     twoInDay, hideAsJolly, excludeTypes)
-- ============================================================

local TIMEOUT = 2000000          -- raised; bitmask ops are much cheaper
local args    = {...}
local conf    = args[1].config
local profileName = args[1].profileName
local profileId   = args[1].profileId

PGC.print('Got profile name, ', profileName, "\n")

-- ---- Bitwise helpers (Lua 5.1 compatible) ------------------
-- Use the 'bit' library (available in LuaJIT and most Lua 5.1
-- sandboxes). Falls back to pure-Lua if not present.
local band, bor, bxor, lshift
if bit then
  band   = bit.band
  bor    = bit.bor
  bxor   = bit.bxor
  lshift = bit.lshift
elseif bit32 then        -- Lua 5.2 standard library
  band   = bit32.band
  bor    = bit32.bor
  bxor   = bit32.bxor
  lshift = bit32.lshift
else
  -- Pure-Lua fallback (slower, but correct for 31-bit values)
  lshift = function(n, s) return n * (2 ^ s) end
  band   = function(a, b)
    local r, p = 0, 1
    for _ = 1, 31 do
      local ra = a % 2; a = (a - ra) / 2
      local rb = b % 2; b = (b - rb) / 2
      if ra == 1 and rb == 1 then r = r + p end
      p = p * 2
    end
    return r
  end
  bor    = function(a, b)
    local r, p = 0, 1
    for _ = 1, 31 do
      local ra = a % 2; a = (a - ra) / 2
      local rb = b % 2; b = (b - rb) / 2
      if ra == 1 or rb == 1 then r = r + p end
      p = p * 2
    end
    return r
  end
  bxor   = function(a, b)
    local r, p = 0, 1
    for _ = 1, 31 do
      local ra = a % 2; a = (a - ra) / 2
      local rb = b % 2; b = (b - rb) / 2
      if ra ~= rb then r = r + p end
      p = p * 2
    end
    return r
  end
end

-- ---- Letter → bit-position mapping -------------------------
-- 31 valid GC-code characters (no I, L, O, S, U)
local LETTERS = {
  'A','B','C','D','E','F','G','H','J','K','M','N','P','Q','R',
  'T','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9'
}
local FULL_MASK = 0
local letter_bit = {}            -- char → bitmask (power of 2)
for i, ch in ipairs(LETTERS) do
  local b = lshift(1, i - 1)    -- 1 << (i-1), Lua-5.1-safe
  letter_bit[ch] = b
  FULL_MASK = bor(FULL_MASK, b)
end
-- FULL_MASK == 2^31 - 1  (all 31 bits set)

-- ---- Helper: compute bitmask for a code string --------------
local function codeMask(code)
  local m = 0
  for i = 1, #code do
    local b = letter_bit[code:sub(i, i)]
    if b then m = bor(m, b) end
  end
  return m
end

-- ---- Helper: count set bits (popcount) ----------------------
local function popcount(n)
  local c = 0
  while n ~= 0 do
    n = band(n, n - 1)   -- clear lowest set bit
    c = c + 1
  end
  return c
end

-- ---- Helper: does a string contain duplicate characters? ----
local function hasDouble(str)
  local seen = {}
  for i = 1, #str do
    local ch = str:sub(i, i)
    if seen[ch] then return true end
    seen[ch] = true
  end
  return false
end

-- ---- Excluded types ----------------------------------------
local excludedTypes = {}
if conf.excludeTypes then
  for _, et in ipairs(conf.excludeTypes) do
    excludedTypes[et] = true
  end
end

-- ---- Fetch data --------------------------------------------
local finds = PGC.GetFinds(profileId, {
  fields = {'gccode', 'cache_name', 'visitdate', 'owner_id', 'type'},
  order  = 'OLDESTFIRST',
  filter = filter
})
local hides = PGC.GetHides(profileId, {fields = {'gccode', 'cache_name', 'hidden'}})
for _, h in ipairs(hides) do
  h.visitdate = "MY OWN"
end

if conf.oneHide then
  for _, h in ipairs(hides) do
    table.insert(finds, {
      gccode     = h.gccode,
      cache_name = h.cache_name,
      visitdate  = "MY OWN",
      owned      = true,
      owner_id   = profileId
    })
  end
end

PGC.print("finds: ", #finds, "\n")

-- ---- Pre-process: filter, attach mask ----------------------
local function preprocessList(list, useExcludeTypes)
  local result = {}
  for _, v in ipairs(list) do
    if useExcludeTypes and excludedTypes[v.type] then
      -- skip excluded type
    else
      local code = v.gccode:sub(3, -1)
      if not hasDouble(code) then
        v.code = code
        v.len  = #code
        v.mask = codeMask(code)
        -- mask must equal FULL_MASK restricted to its own bits,
        -- i.e. no letter appears twice → guaranteed by hasDouble check
        table.insert(result, v)
      end
    end
  end
  return result
end

local filtered_finds = preprocessList(finds, true)
local filtered_hides = preprocessList(hides, false)

PGC.print("not doubles: ", #filtered_finds, "\n")

-- ---- Deduplication by bitmask ------------------------------
-- Keep oldest (first) representative for each unique mask.
-- This is the key scalability improvement: 100 k finds collapse
-- to at most a few thousand distinct masks.
local function dedup(list)
  local seen = {}
  local result = {}
  for _, v in ipairs(list) do
    if not seen[v.mask] then
      seen[v.mask] = true
      table.insert(result, v)
    end
  end
  return result
end

-- ---- Special-condition check (unchanged logic) -------------
local function check_for_special_conditions(winner)
  if conf.oneHide then
    local countOwn = 0
    for _, v in ipairs(winner) do
      if v.owned then countOwn = countOwn + 1 end
    end
    if countOwn == 1 then return true end
    if countOwn > 1  then return false end
  end
  if conf.twoInDay then
    for i, v in ipairs(winner) do
      if not v.owned then
        for j = i + 1, #winner do
          local v2 = winner[j]
          if not v2.owned and v2.visitdate == v.visitdate then
            return true
          end
        end
      end
    end
  end
  if conf.twoInDay or conf.oneHide then return false end
  return true
end

-- ---- Global search state -----------------------------------
local winner   = {}
local odometer = 0
local curdate  -- used by hideAsJolly branch

-- ---- Precompute "union of all masks" prefix sums -----------
-- For a sorted candidate list we build a suffix-union array:
-- suffix_union = mask1 | mask2 | ... | mask[#list]  for j>=i
-- Used for forward-checking: if (curMask | suffix_union) ~= FULL_MASK
-- then no solution can be found from position i onward → prune.
local function buildSuffixUnion(list)
  local su = {}
  local acc = 0
  for i = #list, 1, -1 do
    acc = bor(acc, list.mask)
    su = acc
  end
  return su
end

-- ---- Core recursive search (bitmask edition) ---------------
local function add(list, suffix_union, n, curMask)
  -- Forward-check: can remaining candidates cover all missing bits?
  if n <= #list then
    local needed = bxor(FULL_MASK, curMask)   -- bits still required
    if band(suffix_union[n], needed) ~= needed then
      return false   -- impossible to complete → prune entire branch
    end
  end

  repeat
    -- Advance past candidates that overlap with curMask
    while n <= #list and band(list[n].mask, curMask) ~= 0 do
      n = n + 1
    end

    -- Forward-check again after skipping
    if n > #list then
      -- hideAsJolly fallback: try a hide to fill the remaining gap
      if conf.hideAsJolly then
        local needed = bxor(FULL_MASK, curMask)
        local pc = popcount(needed)
        if pc <= 6 then
          for _, h in ipairs(filtered_hides) do
            if  h.mask == needed
            and h.hidden < curdate
            then
              table.insert(winner, h)
              return true
            end
          end
        end
      end
      return false
    end

    -- Forward-check with remaining suffix
    local needed = bxor(FULL_MASK, curMask)
    if band(suffix_union[n], needed) ~= needed then
      return false
    end

    local v       = list[n]
    local newMask = bor(curMask, v.mask)

    odometer = odometer + 1
    if odometer > TIMEOUT then return false end

    table.insert(winner, v)

    if newMask == FULL_MASK and check_for_special_conditions(winner) then
      PGC.print("GOT IT: ")
      return true
    end

    -- Build sub-list: only candidates compatible with newMask
    -- (no overlapping bits) from position n+1 onward
    local sublist = {}
    for i = n + 1, #list do
      if band(list.mask, newMask) == 0 then
        table.insert(sublist, list)
      end
    end

    local sub_su = buildSuffixUnion(sublist)
    if add(sublist, sub_su, 1, newMask) then return true end

    winner[#winner] = nil
    n = n + 1
  until false
end

local function search(tab)
  -- Sort by popcount descending: try to cover most bits first
  table.sort(tab, function(a, b) return popcount(a.mask) > popcount(b.mask) end)
  -- Deduplicate
  tab = dedup(tab)
  PGC.print("deduplicated candidates: ", #tab, "\n")
  local su = buildSuffixUnion(tab)
  for i = 1, #tab do
    if add(tab, su, i, 0) then return true end
    if odometer > TIMEOUT then return false end
  end
  return false
end

-- ---- Main search -------------------------------------------
local ok = false

if conf.inOneDay then
  -- Group by visit date, sort within each day oldest-first
  local find_per_day = {}
  for _, v in ipairs(filtered_finds) do
    if not find_per_day[v.visitdate] then find_per_day[v.visitdate] = {} end
    table.insert(find_per_day[v.visitdate], v)
  end
  for d, tab in pairs(find_per_day) do
    curdate = d
    -- Need at least 6 caches on one day (min codes cover 6 chars each = 36 > 31)
    -- Original used >=6; keep that guard
    if #tab >= 6 then
      -- Sort by visitdate then mask-length inside day (original order preserved
      -- as tiebreaker; search() will re-sort by popcount)
      table.sort(tab, function(a, b)
        return a.visitdate < b.visitdate
            or (a.visitdate == b.visitdate and a.len < b.len)
      end)
      if search(tab) then ok = true; break end
    end
  end
else
  ok = search(filtered_finds)
end

-- ---- Output ------------------------------------------------
PGC.print("ok=", ok, "\n")
PGC.print("odometer=", odometer, "\n")

table.sort(winner, function(a, b) return a.visitdate < b.visitdate end)

local log  = {"I have found the following caches that fulfills the challenge:"}
local html = {"You have found the following caches that fulfills the challenge:<table>"}

for _, v in ipairs(winner) do
  PGC.print(v.code, "  ", v.visitdate, "  ", v.cache_name, "\n")
  table.insert(log,  v.visitdate .. "  " .. v.code .. "  " .. v.cache_name)
  table.insert(html, "<tr><td>" .. v.visitdate .. "</td><td>" .. v.code
                     .. "</td><td>" .. v.cache_name .. "</td></tr>\n")
end
table.insert(html, "</table>")

if not ok and odometer >= TIMEOUT then
  return {
    ok   = false,
    log  = false,
    html = "<h2>Attention, this can be a false negative as the script was not able to check all combinations.</h2>Please check it manually"
  }
end

log  = table.concat(log,  "\n")
html = table.concat(html)

if not ok then html = "" end
return { ok = ok, log = log, html = html }

Would it be possible to replace the code? I don’t have permission to do so myself, otherwise I would do it myself.

Best regards,
ondraszch
Re: Checker-Update needed for GC5C2AE
April 19, 2026 12:52PM
Checker scripts run in a sandbox using LUA 5.1, see https://project-gc.com/forum/read?7,16.
This means:
- no bit operations possible
- no use of libraries possible

I had a look at this kind of challenge checker script myself a couple of months ago, because a friend of mine told me that the checker was timing out.

Due to the missing bit operations I couldn't make my script much faster than the original once, so I gave up.
Re: Checker-Update needed for GC5C2AE
April 19, 2026 02:10PM
using the second script:
[string ""]:53: attempt to perform arithmetic on local 'b' (a nil value)
Re: Checker-Update needed for GC5C2AE
April 19, 2026 03:32PM
The cause was this: the previous code attempted to use `bit` or `bit32` and, if these were missing, fell back on a fallback – but the fallback code itself contained an error: `b` was used both as a function parameter and as a loop variable, which in Lua results in `nil` as soon as the value becomes zero.
The new version requires no external libraries:

No bit, no bit32, no lshift
band/bor/bxor implemented purely via math.floor and %
FULL_MASK is constructed via simple addition (works because all bits are disjoint)
POW2[] table pre-calculated once, no 2^i in hot loops

Here is the code corrected by Claude:

-- ============================================================
-- Challenge Checker: "Sei deines eigenen Glückes Schmied"
-- GC5C2AE  –  optimized version (Lua 5.1 / bit-library compatible)
-- Changes vs. original:
--   * Bitmask arithmetic instead of string pattern matching
--   * Deduplication: one representative cache per unique bitmask
--   * Candidates sorted by popcount descending (greedy first)
--   * Forward-checking / pruning: abort branch early when
--     remaining candidates can no longer cover missing bits
--   * All original features preserved (inOneDay, oneHide,
--     twoInDay, hideAsJolly, excludeTypes)
-- ============================================================

local TIMEOUT = 2000000          -- raised; bitmask ops are much cheaper
local args    = {...}
local conf    = args[1].config
local profileName = args[1].profileName
local profileId   = args[1].profileId

PGC.print('Got profile name, ', profileName, "\n")

-- ---- Pure-Lua bitwise helpers (no external library needed) --
-- Works in any Lua 5.1+ sandbox without bit, bit32, or lshift.
-- Correct for non-negative integers up to 2^31-1.

local function band(a, b)
  local r, p = 0, 1
  while a > 0 and b > 0 do
    local ra = a % 2; local rb = b % 2
    if ra == 1 and rb == 1 then r = r + p end
    a = math.floor(a / 2); b = math.floor(b / 2); p = p * 2
  end
  return r
end

local function bor(a, b)
  local r, p = 0, 1
  while a > 0 or b > 0 do
    local ra = a % 2; local rb = b % 2
    if ra == 1 or rb == 1 then r = r + p end
    a = math.floor(a / 2); b = math.floor(b / 2); p = p * 2
  end
  return r
end

local function bxor(a, b)
  local r, p = 0, 1
  while a > 0 or b > 0 do
    local ra = a % 2; local rb = b % 2
    if ra ~= rb then r = r + p end
    a = math.floor(a / 2); b = math.floor(b / 2); p = p * 2
  end
  return r
end

-- ---- Letter → bit-position mapping -------------------------
-- 31 valid GC-code characters (no I, L, O, S, U)
local LETTERS = {
  'A','B','C','D','E','F','G','H','J','K','M','N','P','Q','R',
  'T','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9'
}
-- Precompute powers of 2 (avoids repeated exponentiation in loops)
local POW2 = {}
for i = 0, 30 do POW2 = 2^i end

local FULL_MASK = 0
local letter_bit = {}
for i, ch in ipairs(LETTERS) do
  local b = POW2[i - 1]
  letter_bit[ch] = b
  FULL_MASK = FULL_MASK + b   -- bits are disjoint, addition == bor here
end
-- FULL_MASK == 2^31 - 1  (all 31 bits set)

-- ---- Helper: compute bitmask for a code string --------------
local function codeMask(code)
  local m = 0
  for i = 1, #code do
    local b = letter_bit[code:sub(i, i)]
    if b then m = bor(m, b) end
  end
  return m
end

-- ---- Helper: count set bits (popcount) ----------------------
local function popcount(n)
  local c = 0
  while n ~= 0 do
    n = band(n, n - 1)   -- clear lowest set bit
    c = c + 1
  end
  return c
end

-- ---- Helper: does a string contain duplicate characters? ----
local function hasDouble(str)
  local seen = {}
  for i = 1, #str do
    local ch = str:sub(i, i)
    if seen[ch] then return true end
    seen[ch] = true
  end
  return false
end

-- ---- Excluded types ----------------------------------------
local excludedTypes = {}
if conf.excludeTypes then
  for _, et in ipairs(conf.excludeTypes) do
    excludedTypes[et] = true
  end
end

-- ---- Fetch data --------------------------------------------
local finds = PGC.GetFinds(profileId, {
  fields = {'gccode', 'cache_name', 'visitdate', 'owner_id', 'type'},
  order  = 'OLDESTFIRST',
  filter = filter
})
local hides = PGC.GetHides(profileId, {fields = {'gccode', 'cache_name', 'hidden'}})
for _, h in ipairs(hides) do
  h.visitdate = "MY OWN"
end

if conf.oneHide then
  for _, h in ipairs(hides) do
    table.insert(finds, {
      gccode     = h.gccode,
      cache_name = h.cache_name,
      visitdate  = "MY OWN",
      owned      = true,
      owner_id   = profileId
    })
  end
end

PGC.print("finds: ", #finds, "\n")

-- ---- Pre-process: filter, attach mask ----------------------
local function preprocessList(list, useExcludeTypes)
  local result = {}
  for _, v in ipairs(list) do
    if useExcludeTypes and excludedTypes[v.type] then
      -- skip excluded type
    else
      local code = v.gccode:sub(3, -1)
      if not hasDouble(code) then
        v.code = code
        v.len  = #code
        v.mask = codeMask(code)
        -- mask must equal FULL_MASK restricted to its own bits,
        -- i.e. no letter appears twice → guaranteed by hasDouble check
        table.insert(result, v)
      end
    end
  end
  return result
end

local filtered_finds = preprocessList(finds, true)
local filtered_hides = preprocessList(hides, false)

PGC.print("not doubles: ", #filtered_finds, "\n")

-- ---- Deduplication by bitmask ------------------------------
-- Keep oldest (first) representative for each unique mask.
-- This is the key scalability improvement: 100 k finds collapse
-- to at most a few thousand distinct masks.
local function dedup(list)
  local seen = {}
  local result = {}
  for _, v in ipairs(list) do
    if not seen[v.mask] then
      seen[v.mask] = true
      table.insert(result, v)
    end
  end
  return result
end

-- ---- Special-condition check (unchanged logic) -------------
local function check_for_special_conditions(winner)
  if conf.oneHide then
    local countOwn = 0
    for _, v in ipairs(winner) do
      if v.owned then countOwn = countOwn + 1 end
    end
    if countOwn == 1 then return true end
    if countOwn > 1  then return false end
  end
  if conf.twoInDay then
    for i, v in ipairs(winner) do
      if not v.owned then
        for j = i + 1, #winner do
          local v2 = winner[j]
          if not v2.owned and v2.visitdate == v.visitdate then
            return true
          end
        end
      end
    end
  end
  if conf.twoInDay or conf.oneHide then return false end
  return true
end

-- ---- Global search state -----------------------------------
local winner   = {}
local odometer = 0
local curdate  -- used by hideAsJolly branch

-- ---- Precompute "union of all masks" prefix sums -----------
-- For a sorted candidate list we build a suffix-union array:
-- suffix_union = mask1 | mask2 | ... | mask[#list]  for j>=i
-- Used for forward-checking: if (curMask | suffix_union) ~= FULL_MASK
-- then no solution can be found from position i onward → prune.
local function buildSuffixUnion(list)
  local su = {}
  local acc = 0
  for i = #list, 1, -1 do
    acc = bor(acc, list.mask)
    su = acc
  end
  return su
end

-- ---- Core recursive search (bitmask edition) ---------------
local function add(list, suffix_union, n, curMask)
  -- Forward-check: can remaining candidates cover all missing bits?
  if n <= #list then
    local needed = bxor(FULL_MASK, curMask)   -- bits still required
    if band(suffix_union[n], needed) ~= needed then
      return false   -- impossible to complete → prune entire branch
    end
  end

  repeat
    -- Advance past candidates that overlap with curMask
    while n <= #list and band(list[n].mask, curMask) ~= 0 do
      n = n + 1
    end

    -- Forward-check again after skipping
    if n > #list then
      -- hideAsJolly fallback: try a hide to fill the remaining gap
      if conf.hideAsJolly then
        local needed = bxor(FULL_MASK, curMask)
        local pc = popcount(needed)
        if pc <= 6 then
          for _, h in ipairs(filtered_hides) do
            if  h.mask == needed
            and h.hidden < curdate
            then
              table.insert(winner, h)
              return true
            end
          end
        end
      end
      return false
    end

    -- Forward-check with remaining suffix
    local needed = bxor(FULL_MASK, curMask)
    if band(suffix_union[n], needed) ~= needed then
      return false
    end

    local v       = list[n]
    local newMask = bor(curMask, v.mask)

    odometer = odometer + 1
    if odometer > TIMEOUT then return false end

    table.insert(winner, v)

    if newMask == FULL_MASK and check_for_special_conditions(winner) then
      PGC.print("GOT IT: ")
      return true
    end

    -- Build sub-list: only candidates compatible with newMask
    -- (no overlapping bits) from position n+1 onward
    local sublist = {}
    for i = n + 1, #list do
      if band(list.mask, newMask) == 0 then
        table.insert(sublist, list)
      end
    end

    local sub_su = buildSuffixUnion(sublist)
    if add(sublist, sub_su, 1, newMask) then return true end

    winner[#winner] = nil
    n = n + 1
  until false
end

local function search(tab)
  -- Sort by popcount descending: try to cover most bits first
  table.sort(tab, function(a, b) return popcount(a.mask) > popcount(b.mask) end)
  -- Deduplicate
  tab = dedup(tab)
  PGC.print("deduplicated candidates: ", #tab, "\n")
  local su = buildSuffixUnion(tab)
  for i = 1, #tab do
    if add(tab, su, i, 0) then return true end
    if odometer > TIMEOUT then return false end
  end
  return false
end

-- ---- Main search -------------------------------------------
local ok = false

if conf.inOneDay then
  -- Group by visit date, sort within each day oldest-first
  local find_per_day = {}
  for _, v in ipairs(filtered_finds) do
    if not find_per_day[v.visitdate] then find_per_day[v.visitdate] = {} end
    table.insert(find_per_day[v.visitdate], v)
  end
  for d, tab in pairs(find_per_day) do
    curdate = d
    -- Need at least 6 caches on one day (min codes cover 6 chars each = 36 > 31)
    -- Original used >=6; keep that guard
    if #tab >= 6 then
      -- Sort by visitdate then mask-length inside day (original order preserved
      -- as tiebreaker; search() will re-sort by popcount)
      table.sort(tab, function(a, b)
        return a.visitdate < b.visitdate
            or (a.visitdate == b.visitdate and a.len < b.len)
      end)
      if search(tab) then ok = true; break end
    end
  end
else
  ok = search(filtered_finds)
end

-- ---- Output ------------------------------------------------
PGC.print("ok=", ok, "\n")
PGC.print("odometer=", odometer, "\n")

table.sort(winner, function(a, b) return a.visitdate < b.visitdate end)

local log  = {"I have found the following caches that fulfills the challenge:"}
local html = {"You have found the following caches that fulfills the challenge:<table>"}

for _, v in ipairs(winner) do
  PGC.print(v.code, "  ", v.visitdate, "  ", v.cache_name, "\n")
  table.insert(log,  v.visitdate .. "  " .. v.code .. "  " .. v.cache_name)
  table.insert(html, "<tr><td>" .. v.visitdate .. "</td><td>" .. v.code
                     .. "</td><td>" .. v.cache_name .. "</td></tr>\n")
end
table.insert(html, "</table>")

if not ok and odometer >= TIMEOUT then
  return {
    ok   = false,
    log  = false,
    html = "<h2>Attention, this can be a false negative as the script was not able to check all combinations.</h2>Please check it manually"
  }
end

log  = table.concat(log,  "\n")
html = table.concat(html)

if not ok then html = "" end
return { ok = ok, log = log, html = html }
Re: Checker-Update needed for GC5C2AE
April 19, 2026 03:39PM
[string ""]:69: attempt to index local 'POW2' (a number value)
Re: Checker-Update needed for GC5C2AE
April 19, 2026 04:01PM
Claude:
That can only mean that the PGC sandbox has already taken the name POW2 as a global variable – for example, as a number. Since `local POW2 = {}` creates a new local variable, that should actually be safe... unless the sandbox prohibits or overrides local variables with specific names.
Simplest solution: Avoid the name POW2 and incorporate the powers directly into letter_bit, without an intermediate table.

-- ============================================================
-- Challenge Checker: "Sei deines eigenen Glückes Schmied"
-- GC5C2AE  –  optimized version (Lua 5.1 / bit-library compatible)
-- Changes vs. original:
--   * Bitmask arithmetic instead of string pattern matching
--   * Deduplication: one representative cache per unique bitmask
--   * Candidates sorted by popcount descending (greedy first)
--   * Forward-checking / pruning: abort branch early when
--     remaining candidates can no longer cover missing bits
--   * All original features preserved (inOneDay, oneHide,
--     twoInDay, hideAsJolly, excludeTypes)
-- ============================================================

local TIMEOUT = 2000000          -- raised; bitmask ops are much cheaper
local args    = {...}
local conf    = args[1].config
local profileName = args[1].profileName
local profileId   = args[1].profileId

PGC.print('Got profile name, ', profileName, "\n")

-- ---- Pure-Lua bitwise helpers (no external library needed) --
-- Works in any Lua 5.1+ sandbox without bit, bit32, or lshift.
-- Correct for non-negative integers up to 2^31-1.

local function band(a, b)
  local r, p = 0, 1
  while a > 0 and b > 0 do
    local ra = a % 2; local rb = b % 2
    if ra == 1 and rb == 1 then r = r + p end
    a = math.floor(a / 2); b = math.floor(b / 2); p = p * 2
  end
  return r
end

local function bor(a, b)
  local r, p = 0, 1
  while a > 0 or b > 0 do
    local ra = a % 2; local rb = b % 2
    if ra == 1 or rb == 1 then r = r + p end
    a = math.floor(a / 2); b = math.floor(b / 2); p = p * 2
  end
  return r
end

local function bxor(a, b)
  local r, p = 0, 1
  while a > 0 or b > 0 do
    local ra = a % 2; local rb = b % 2
    if ra ~= rb then r = r + p end
    a = math.floor(a / 2); b = math.floor(b / 2); p = p * 2
  end
  return r
end

-- ---- Letter → bit-position mapping -------------------------
-- 31 valid GC-code characters (no I, L, O, S, U)
local LETTERS = {
  'A','B','C','D','E','F','G','H','J','K','M','N','P','Q','R',
  'T','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9'
}
local FULL_MASK = 0
local letter_bit = {}
do
  local p = 1   -- p = 2^(i-1), starts at 2^0 = 1
  for _, ch in ipairs(LETTERS) do
    letter_bit[ch] = p
    FULL_MASK = FULL_MASK + p   -- bits are disjoint, addition == bor here
    p = p * 2
  end
end
-- FULL_MASK == 2^31 - 1  (all 31 bits set)

-- ---- Helper: compute bitmask for a code string --------------
local function codeMask(code)
  local m = 0
  for i = 1, #code do
    local b = letter_bit[code:sub(i, i)]
    if b then m = bor(m, b) end
  end
  return m
end

-- ---- Helper: count set bits (popcount) ----------------------
local function popcount(n)
  local c = 0
  while n ~= 0 do
    n = band(n, n - 1)   -- clear lowest set bit
    c = c + 1
  end
  return c
end

-- ---- Helper: does a string contain duplicate characters? ----
local function hasDouble(str)
  local seen = {}
  for i = 1, #str do
    local ch = str:sub(i, i)
    if seen[ch] then return true end
    seen[ch] = true
  end
  return false
end

-- ---- Excluded types ----------------------------------------
local excludedTypes = {}
if conf.excludeTypes then
  for _, et in ipairs(conf.excludeTypes) do
    excludedTypes[et] = true
  end
end

-- ---- Fetch data --------------------------------------------
local finds = PGC.GetFinds(profileId, {
  fields = {'gccode', 'cache_name', 'visitdate', 'owner_id', 'type'},
  order  = 'OLDESTFIRST',
  filter = filter
})
local hides = PGC.GetHides(profileId, {fields = {'gccode', 'cache_name', 'hidden'}})
for _, h in ipairs(hides) do
  h.visitdate = "MY OWN"
end

if conf.oneHide then
  for _, h in ipairs(hides) do
    table.insert(finds, {
      gccode     = h.gccode,
      cache_name = h.cache_name,
      visitdate  = "MY OWN",
      owned      = true,
      owner_id   = profileId
    })
  end
end

PGC.print("finds: ", #finds, "\n")

-- ---- Pre-process: filter, attach mask ----------------------
local function preprocessList(list, useExcludeTypes)
  local result = {}
  for _, v in ipairs(list) do
    if useExcludeTypes and excludedTypes[v.type] then
      -- skip excluded type
    else
      local code = v.gccode:sub(3, -1)
      if not hasDouble(code) then
        v.code = code
        v.len  = #code
        v.mask = codeMask(code)
        -- mask must equal FULL_MASK restricted to its own bits,
        -- i.e. no letter appears twice → guaranteed by hasDouble check
        table.insert(result, v)
      end
    end
  end
  return result
end

local filtered_finds = preprocessList(finds, true)
local filtered_hides = preprocessList(hides, false)

PGC.print("not doubles: ", #filtered_finds, "\n")

-- ---- Deduplication by bitmask ------------------------------
-- Keep oldest (first) representative for each unique mask.
-- This is the key scalability improvement: 100 k finds collapse
-- to at most a few thousand distinct masks.
local function dedup(list)
  local seen = {}
  local result = {}
  for _, v in ipairs(list) do
    if not seen[v.mask] then
      seen[v.mask] = true
      table.insert(result, v)
    end
  end
  return result
end

-- ---- Special-condition check (unchanged logic) -------------
local function check_for_special_conditions(winner)
  if conf.oneHide then
    local countOwn = 0
    for _, v in ipairs(winner) do
      if v.owned then countOwn = countOwn + 1 end
    end
    if countOwn == 1 then return true end
    if countOwn > 1  then return false end
  end
  if conf.twoInDay then
    for i, v in ipairs(winner) do
      if not v.owned then
        for j = i + 1, #winner do
          local v2 = winner[j]
          if not v2.owned and v2.visitdate == v.visitdate then
            return true
          end
        end
      end
    end
  end
  if conf.twoInDay or conf.oneHide then return false end
  return true
end

-- ---- Global search state -----------------------------------
local winner   = {}
local odometer = 0
local curdate  -- used by hideAsJolly branch

-- ---- Precompute "union of all masks" prefix sums -----------
-- For a sorted candidate list we build a suffix-union array:
-- suffix_union = mask1 | mask2 | ... | mask[#list]  for j>=i
-- Used for forward-checking: if (curMask | suffix_union) ~= FULL_MASK
-- then no solution can be found from position i onward → prune.
local function buildSuffixUnion(list)
  local su = {}
  local acc = 0
  for i = #list, 1, -1 do
    acc = bor(acc, list.mask)
    su = acc
  end
  return su
end

-- ---- Core recursive search (bitmask edition) ---------------
local function add(list, suffix_union, n, curMask)
  -- Forward-check: can remaining candidates cover all missing bits?
  if n <= #list then
    local needed = bxor(FULL_MASK, curMask)   -- bits still required
    if band(suffix_union[n], needed) ~= needed then
      return false   -- impossible to complete → prune entire branch
    end
  end

  repeat
    -- Advance past candidates that overlap with curMask
    while n <= #list and band(list[n].mask, curMask) ~= 0 do
      n = n + 1
    end

    -- Forward-check again after skipping
    if n > #list then
      -- hideAsJolly fallback: try a hide to fill the remaining gap
      if conf.hideAsJolly then
        local needed = bxor(FULL_MASK, curMask)
        local pc = popcount(needed)
        if pc <= 6 then
          for _, h in ipairs(filtered_hides) do
            if  h.mask == needed
            and h.hidden < curdate
            then
              table.insert(winner, h)
              return true
            end
          end
        end
      end
      return false
    end

    -- Forward-check with remaining suffix
    local needed = bxor(FULL_MASK, curMask)
    if band(suffix_union[n], needed) ~= needed then
      return false
    end

    local v       = list[n]
    local newMask = bor(curMask, v.mask)

    odometer = odometer + 1
    if odometer > TIMEOUT then return false end

    table.insert(winner, v)

    if newMask == FULL_MASK and check_for_special_conditions(winner) then
      PGC.print("GOT IT: ")
      return true
    end

    -- Build sub-list: only candidates compatible with newMask
    -- (no overlapping bits) from position n+1 onward
    local sublist = {}
    for i = n + 1, #list do
      if band(list.mask, newMask) == 0 then
        table.insert(sublist, list)
      end
    end

    local sub_su = buildSuffixUnion(sublist)
    if add(sublist, sub_su, 1, newMask) then return true end

    winner[#winner] = nil
    n = n + 1
  until false
end

local function search(tab)
  -- Sort by popcount descending: try to cover most bits first
  table.sort(tab, function(a, b) return popcount(a.mask) > popcount(b.mask) end)
  -- Deduplicate
  tab = dedup(tab)
  PGC.print("deduplicated candidates: ", #tab, "\n")
  local su = buildSuffixUnion(tab)
  for i = 1, #tab do
    if add(tab, su, i, 0) then return true end
    if odometer > TIMEOUT then return false end
  end
  return false
end

-- ---- Main search -------------------------------------------
local ok = false

if conf.inOneDay then
  -- Group by visit date, sort within each day oldest-first
  local find_per_day = {}
  for _, v in ipairs(filtered_finds) do
    if not find_per_day[v.visitdate] then find_per_day[v.visitdate] = {} end
    table.insert(find_per_day[v.visitdate], v)
  end
  for d, tab in pairs(find_per_day) do
    curdate = d
    -- Need at least 6 caches on one day (min codes cover 6 chars each = 36 > 31)
    -- Original used >=6; keep that guard
    if #tab >= 6 then
      -- Sort by visitdate then mask-length inside day (original order preserved
      -- as tiebreaker; search() will re-sort by popcount)
      table.sort(tab, function(a, b)
        return a.visitdate < b.visitdate
            or (a.visitdate == b.visitdate and a.len < b.len)
      end)
      if search(tab) then ok = true; break end
    end
  end
else
  ok = search(filtered_finds)
end

-- ---- Output ------------------------------------------------
PGC.print("ok=", ok, "\n")
PGC.print("odometer=", odometer, "\n")

table.sort(winner, function(a, b) return a.visitdate < b.visitdate end)

local log  = {"I have found the following caches that fulfills the challenge:"}
local html = {"You have found the following caches that fulfills the challenge:<table>"}

for _, v in ipairs(winner) do
  PGC.print(v.code, "  ", v.visitdate, "  ", v.cache_name, "\n")
  table.insert(log,  v.visitdate .. "  " .. v.code .. "  " .. v.cache_name)
  table.insert(html, "<tr><td>" .. v.visitdate .. "</td><td>" .. v.code
                     .. "</td><td>" .. v.cache_name .. "</td></tr>\n")
end
table.insert(html, "</table>")

if not ok and odometer >= TIMEOUT then
  return {
    ok   = false,
    log  = false,
    html = "<h2>Attention, this can be a false negative as the script was not able to check all combinations.</h2>Please check it manually"
  }
end

log  = table.concat(log,  "\n")
html = table.concat(html)

if not ok then html = "" end
return { ok = ok, log = log, html = html }
Re: Checker-Update needed for GC5C2AE
April 19, 2026 04:11PM
[string ""]:38: attempt to compare number with nil

I don't feel like debugging for your smart guy Claudius

I known that my script can be optimized, however as I've wrote above, this type of challenge is no more allowed for new caches, so there is no interest to improve it. If you can give me WORKING code, I can update it, but I cannot debug AI buggy code.
Re: Checker-Update needed for GC5C2AE
April 19, 2026 04:18PM
I’d like to do that, but I don’t have the means to test it. Perhaps the checker function could be updated to include a year selection? That should help narrow down the errors when there are large numbers of results.
Re: Checker-Update needed for GC5C2AE
April 19, 2026 04:52PM
Ok, now I understand your problem. The problem has a name: Bergfex2000 - your reviewer :-) Now I understand why do you need this checker.
However, your reviewer has also another question - how to find and plan the right combination of caches. We can optimize the checker to avoid timeouts, but we cannot do any help to geocachers to plan the trip.
I think that your reviewer is right, it's impossible to paln to fulfill the challenge. I cannot help you.

Also, I've found that there are two checkers for your challenge, both based on my script
https://project-gc.com/Challenges/GC5C2AE/18262
and
https://project-gc.com/Challenges/GC5C2AE/79495
The first one is mine, the second one is wrong as it does not have the condition with own cache and does not exclude virtuals and earths. It was made by Hugh. You should contact him and ask him to delete the tag (I cannot do it).
Re: Checker-Update needed for GC5C2AE
April 19, 2026 05:02PM
Bergfex2000 ius not a reviewer account, but just a normal Premium Member.
Re: Checker-Update needed for GC5C2AE
April 19, 2026 04:57PM
One last try:

local TIMEOUT=1000000
local args={...}
local conf = args[1].config
local profileName = args[1].profileName
local profileId = args[1].profileId
PGC.print('Got profile name, ', profileName, "\n")

local letters={'A','B','C','D','E','F','G','H','J','K','M','N','P','Q','R','T','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9'}
local ALL_CHARS='ABCDEFGHJKMNPQRTVWXYZ0123456789'  -- 31 chars

function hasDouble(str)
  local dbl={}
  for i=1,str:len() do
    local l=str:sub(i,i)
    if dbl[l] then return true end
    dbl[l]=true
  end
  return false
end

-- Returns true if str1 and str2 share at least one character
function hasOverlap(str1, str2)
  for i=1,str2:len() do
    if str1:find(str2:sub(i,i), 1, true) then return true end
  end
  return false
end

-- Returns true if every character in 'needed' appears in 'pool'
function isSubset(needed, pool)
  for i=1,needed:len() do
    if not pool:find(needed:sub(i,i), 1, true) then return false end
  end
  return true
end

-- Canonical key for deduplication: sort characters of a code
function sortedKey(code)
  local t={}
  for i=1,code:len() do t=code:sub(i,i) end
  table.sort(t)
  return table.concat(t)
end

local excludedTypes={}
if conf.excludeTypes then
  for _,et in ipairs(conf.excludeTypes) do
    excludedTypes[et]=true
  end
end

local finds = PGC.GetFinds(profileId, { fields = {'gccode', 'cache_name', 'visitdate', 'owner_id', 'type'}, order = 'OLDESTFIRST', filter = filter })
local hides = PGC.GetHides(profileId, { fields = {'gccode', 'cache_name', 'hidden'}})
for _,h in ipairs(hides) do
  h.visitdate="MY OWN"
end

if conf.oneHide then
  for _,h in ipairs(hides) do
    table.insert(finds, { gccode=h.gccode, cache_name=h.cache_name, visitdate="MY OWN", owned=true, owner_id=profileId })
  end
end

PGC.print("finds: ",#finds,"\n")

local filtered_finds={}
local cnt=0
for _,v in ipairs(finds) do
  v.code=v.gccode:sub(3,-1)
  v.len=v.code:len()
  v.skey=sortedKey(v.code)
  if not hasDouble(v.code) and excludedTypes[v.type]==nil then
    cnt=cnt+1
    table.insert(filtered_finds,v)
  end
end

local filtered_hides={}
for _,v in ipairs(hides) do
  v.code=v.gccode:sub(3,-1)
  v.len=v.code:len()
  if not hasDouble(v.code) then
    table.insert(filtered_hides,v)
  end
end

PGC.print("not doubles: ",cnt,"\n")

-- Deduplicate filtered_finds by sorted character key.
-- Caches with identical character sets are interchangeable for this challenge;
-- keeping only one per set dramatically shrinks the search space.
local function deduplicate(list)
  local seen={}
  local result={}
  for _,v in ipairs(list) do
    if not seen[v.skey] then
      seen[v.skey]=true
      table.insert(result,v)
    end
  end
  return result
end

-- Build suffix-union table: su is a string containing every character
-- that appears in any candidate from position i to end of list.
-- Used for pruning: if a needed character is absent from su,
-- no combination starting at or after i can cover it -> abort branch.
local function buildSuffixUnion(list)
  local su={}
  local acc={}
  local accStr=''
  for i=#list,1,-1 do
    local code=list.code
    for j=1,code:len() do
      local ch=code:sub(j,j)
      if not acc[ch] then
        acc[ch]=true
        accStr=accStr..ch
      end
    end
    su=accStr
  end
  return su
end

if conf.inOneDay then
  table.sort(filtered_finds,function(a,b) return a.visitdate<b.visitdate or a.visitdate==b.visitdate and a.len<b.len end)
else
  table.sort(filtered_finds,function(a,b) return a.len>b.len end)  -- longest first
end

winner={}
odometer=0

local state={}

function check_for_special_conditions(winner)
  if conf.oneHide then
    local countOwn=0
    for i,v in ipairs(winner) do
      if v.owned then countOwn=countOwn+1 end
    end
    if countOwn==1 then return true end
    if countOwn>1 then return false end
  end
  if conf.twoInDay then
    for i,v in ipairs(winner) do
      if not v.owned then
        for j=i+1,#winner do
          local v2=winner[j]
          if not v2.owned and v2.visitdate==v.visitdate then return true end
        end
      end
    end
  end
  if conf.twoInDay or conf.oneHide then return false end
  return true
end

local curdate
function add(list, su, n, covered)
  repeat
    -- Skip candidates that overlap with already-covered characters
    while n<=#list and hasOverlap(covered, list[n].code) do
      n=n+1
    end

    if n>#list then
      -- hideAsJolly: try a hide to fill the remaining gap
      if conf.hideAsJolly then
        local needed=''
        for i=1,ALL_CHARS:len() do
          local ch=ALL_CHARS:sub(i,i)
          if not covered:find(ch,1,true) then needed=needed..ch end
        end
        if needed:len()<=6 then
          for _,h in ipairs(filtered_hides) do
            if h.len==needed:len()
            and h.hidden<curdate
            and not hasOverlap(covered, h.code)
            and isSubset(needed, h.code)
            then
              table.insert(winner,h)
              return true
            end
          end
        end
      end
      return false
    end

    -- Pruning: check that the suffix union covers all still-needed characters
    local needed=''
    for i=1,ALL_CHARS:len() do
      local ch=ALL_CHARS:sub(i,i)
      if not covered:find(ch,1,true) then needed=needed..ch end
    end
    if needed=='' then
      -- All 31 covered (shouldn't reach here, but safety check)
      if check_for_special_conditions(winner) then return true end
      return false
    end
    if not isSubset(needed, su[n]) then
      return false  -- no solution possible from here
    end

    local v=list[n]
    odometer=odometer+1
    if odometer>TIMEOUT then return false end

    table.insert(winner,v)
    local newCovered=covered..v.code

    if newCovered:len()==31 and check_for_special_conditions(winner) then
      PGC.print("GOT IT: ")
      return true
    end

    -- Recurse: build sub-list of non-overlapping candidates from n+1
    local sublist={}
    for i=n+1,#list do
      if not hasOverlap(newCovered, list.code) then
        table.insert(sublist, list)
      end
    end
    local sub_su=buildSuffixUnion(sublist)

    if add(sublist, sub_su, 1, newCovered) then return true end

    winner[#winner]=nil
    n=n+1
  until false
end

function search(tab)
  tab=deduplicate(tab)
  PGC.print("after dedup: ",#tab,"\n")
  local su=buildSuffixUnion(tab)
  -- Check early: does this set cover all 31 chars at all?
  if #tab==0 or not isSubset(ALL_CHARS, su[1]) then
    return false
  end
  for i=1,#tab do
    if add(tab,su,i,'') then return true end
    if odometer>TIMEOUT then return false end
  end
  return false
end

local ok=false
local log = {}
local html = {}

if conf.inOneDay then
  local find_per_day={}
  for _,v in ipairs(filtered_finds) do
    if find_per_day[v.visitdate]==nil then find_per_day[v.visitdate]={} end
    table.insert(find_per_day[v.visitdate], v)
  end
  for d,tab in pairs(find_per_day) do
    curdate=d
    if #tab>=6 and search(tab) then ok=true; break end
  end
else
  ok=search(filtered_finds)
end

PGC.print("ok=",ok,"\n")
PGC.print("odometer=",odometer,"\n")
table.sort(winner, function(a,b) return a.visitdate<b.visitdate end)
table.insert(log,"I have found the following caches that fulfills the challenge:")
table.insert(html,"You have found the following caches that fulfills the challenge:<table>")
for i,v in ipairs(winner) do
  PGC.print(v.code, "  ", v.visitdate, "  ", v.cache_name, "\n")
  table.insert(log, v.visitdate.."  "..v.code.."  "..v.cache_name)
  table.insert(html,"<tr><td>"..v.visitdate.."</td><td>"..v.code.."</td><td>"..v.cache_name.."</td></tr>\n")
end
table.insert(html,"</table>")

if not ok and odometer>=TIMEOUT then
  return {ok=false, log=false, html="<h2>Attention, this can be a false negative as the script was not able to check all combination.</h2>Please check it manually"}
end

log = table.concat(log, "\n")
html= table.concat(html)

if not ok then html="" end
return { ok = ok, log = log, html = html }

If that doesn’t work, we’ll stop here, as I’ve certainly taken up far too much of your time. A checker was never intended for this challenge, as it’s nearly impossible to complete without planning. Thank you very much for your understanding.
Best regards,
ondraszch
Re: Checker-Update needed for GC5C2AE
April 19, 2026 05:34PM
[string ""]:41: bad argument #1 to 'sort' (table expected, got string)

sorry, Claudius strikes again
Re: Checker-Update needed for GC5C2AE
April 19, 2026 07:52PM
I can look into a custom script again.

The usual GC code provides 5 characters and you need at least 7 caches, so 7*5=35 charaters. That means you need GC codes that are together 4 characters shorter than the usual one to collect the required exaclty 31 characters.

All qualifying caches also must be found on the same day (plus not more than 1 own cache that was not yet archived on that day). This should allow to drop most find days without having to do more detailed tests. I.e. all days where there are not enough caches for that day or you can't save 4 charaters by having shorter GC codes can immediately be dropped.

Only a few found days should be left with the potential to collect excatly 31 characters, which would then need intensive testing.
So I'm hopeful that such a checker won't time out anymore even for players with lots of found caches.
Re: Checker-Update needed for GC5C2AE
April 19, 2026 09:43PM
Can you provide a couple of names that cause timeout?
Re: Checker-Update needed for GC5C2AE
April 19, 2026 10:11PM
Hmm...my friend's nickname doesn't time out on this challenge.
I think she contacted me regarding a different challenge that also used this script, but without the "in one day" option.

However, I get a strange output after 4.5s for Bergfex2000.
Re: Checker-Update needed for GC5C2AE
April 19, 2026 10:38PM
In the meantime I've found some cacher with timeout.
I'm trying to optimize and fix it. First of all I've fixed the timeout that was originally set as a certain limit of tries based on speed of server 10 years ago. Now there is function to calculate the time, so now is based on time (55 seconds). Then I've add the test for at least 5 caches with code shorter than 5. I hope that now no one gets the timeout.
Try it now.
Re: Checker-Update needed for GC5C2AE
April 20, 2026 07:47PM
Guys, you’re amazing! But I don’t want to take up too much of your time. You decide when we’re finished. I (Claudius) have since tried out a planning tool. At least I can test it out myself using Pocket Queries.
Best regards,
ondraszch
Re: Checker-Update needed for GC5C2AE
April 20, 2026 08:46PM
I'm trying to fix it. Thank you, you are right, 4 are enough. Give me a moment
Re: Checker-Update needed for GC5C2AE
April 20, 2026 07:52PM
Now it seems that neither checker is working anymore:

ondraszch does not fulfill challenge Sei deines eigenen Glückes Schmied (Challenge) (GC5C2AE) according to https://project-gc.com/Challenges/GC5C2AE/18262

ondraszch does not fulfill challenge Sei deines eigenen Glückes Schmied (Challenge) (GC5C2AE) according to https://project-gc.com/Challenges/GC5C2AE/79495
Re: Checker-Update needed for GC5C2AE
April 20, 2026 08:09PM
@jpavlik You only need 4 caches with a shorter GC code, not 5 caches.
Typically the challenge is fulfilled with 4x4 characters + 3x5 characters.

This would miss solutions with 3 characters + 2x4 characters + 4x5 characters, but I guess nobody has such a solutions anyways.
Re: Checker-Update needed for GC5C2AE
April 20, 2026 08:50PM
I've fixed it Thank you, you are right, 4 are enough.
Re: Checker-Update needed for GC5C2AE
April 21, 2026 06:50PM
I owe you both at least a beer each. Unfortunately, I’m not really big on events. But I do go to the end-of-year event at the Babisnauer Pappel in Dresden regularly. After a ‘stress test’ with a few >>50k loggers, both checkers are working. Many thanks and best regards, ondraszch

--> The thread can be marked as ‘Resolved’



Numanoid fulfills challenge Sei deines eigenen Glückes Schmied (Challenge) (GC5C2AE) according to https://project-gc.com/Challenges/GC5C2AE/18262

Numanoid fulfills challenge Sei deines eigenen Glückes Schmied (Challenge) (GC5C2AE) according to https://project-gc.com/Challenges/GC5C2AE/79495
Sorry, only registered users may post in this forum.

Click here to login