diff --git a/src/Global.-1.ttslua b/src/Global.-1.ttslua index 65d6c30..de374cc 100644 --- a/src/Global.-1.ttslua +++ b/src/Global.-1.ttslua @@ -232,6 +232,76 @@ analytics = sessions = {} } +----------[ Character sets ]---------- + +digits_table = { +[0]={48,1632,1776,1984,2406,2534,2662,2790,2918,3046,3174,3302,3430,3558,3664,3792,3872,4160,4240,6112,6160,6470,6608,6784,6800,6992,7088,7232,7248,42528,43216,43264,43472,43504,43600,44016,65296}, +[1]={49,1633,1777,1985,2407,2535,2663,2791,2919,3047,3175,3303,3431,3559,3665,3793,3873,4161,4241,6113,6161,6471,6609,6785,6801,6993,7089,7233,7249,42529,43217,43265,43473,43505,43601,44017,65297}, +[2]={50,1634,1778,1986,2408,2536,2664,2792,2920,3048,3176,3304,3432,3560,3666,3794,3874,4162,4242,6114,6162,6472,6610,6786,6802,6994,7090,7234,7250,42530,43218,43266,43474,43506,43602,44018,65298}, +[3]={51,1635,1779,1987,2409,2537,2665,2793,2921,3049,3177,3305,3433,3561,3667,3795,3875,4163,4243,6115,6163,6473,6611,6787,6803,6995,7091,7235,7251,42531,43219,43267,43475,43507,43603,44019,65299}, +[4]={52,1636,1780,1988,2410,2538,2666,2794,2922,3050,3178,3306,3434,3562,3668,3796,3876,4164,4244,6116,6164,6474,6612,6788,6804,6996,7092,7236,7252,42532,43220,43268,43476,43508,43604,44020,65300}, +[5]={53,1637,1781,1989,2411,2539,2667,2795,2923,3051,3179,3307,3435,3563,3669,3797,3877,4165,4245,6117,6165,6475,6613,6789,6805,6997,7093,7237,7253,42533,43221,43269,43477,43509,43605,44021,65301}, +[6]={54,1638,1782,1990,2412,2540,2668,2796,2924,3052,3180,3308,3436,3564,3670,3798,3878,4166,4246,6118,6166,6476,6614,6790,6806,6998,7094,7238,7254,42534,43222,43270,43478,43510,43606,44022,65302}, +[7]={55,1639,1783,1991,2413,2541,2669,2797,2925,3053,3181,3309,3437,3565,3671,3799,3879,4167,4247,6119,6167,6477,6615,6791,6807,6999,7095,7239,7255,42535,43223,43271,43479,43511,43607,44023,65303}, +[8]={56,1640,1784,1992,2414,2542,2670,2798,2926,3054,3182,3310,3438,3566,3672,3800,3880,4168,4248,6120,6168,6478,6616,6792,6808,7000,7096,7240,7256,42536,43224,43272,43480,43512,43608,44024,65304}, +[9]={57,1641,1785,1993,2415,2543,2671,2799,2927,3055,3183,3311,3439,3567,3673,3801,3881,4169,4249,6121,6169,6479,6617,6793,6809,7001,7097,7241,7257,42537,43225,43273,43481,43513,43609,44025,65305} +} + +spaceCharacters = {} +for _, code in pairs({32,160,5760,6158,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8203,8239,8287,12288,65279}) do + spaceCharacters[code] = true +end + +hyphenCharacters = {} +for _, code in pairs({45,8208,8209,8210,8211,8212,8213,11834,11835,65112,65123,65293}) do + hyphenCharacters[code] = true +end + +digitCharacters = {} +for digit, arr in pairs(digits_table) do + for _, code in pairs(arr) do + digitCharacters[code] = tostring(digit) -- strings are interpreted as 'true' for boolean expressions, even empty ones + end +end + +whitespaceCharacters = {} +for _, code in pairs({9,10,11,12,13,28,29,30,31,133,8232,8233}) do + whitespaceCharacters[code] = true +end +for code, _ in pairs(spaceCharacters) do + whitespaceCharacters[code] = true +end + +illegalCharacters = {} +-- put several character ranges into the illegalCharacters table +local illegalCharacterRanges = + { + {0, 64}, -- before uppercase letters + {91, 96}, -- before lowercase letters + {123, 191}, -- before accented letters + {215, 215}, {247, 247}, -- multiplication and division sign + {448, 451}, -- symbols + {688, 767}, -- symbols + } +for _, range in pairs(illegalCharacterRanges) do + local code_st, code_end = table.unpack(range) + for code = code_st, code_end do + illegalCharacters[code] = true + end +end +-- include hyphens +for code, _ in pairs(hyphenCharacters) do + illegalCharacters[code] = true +end +-- include whitespace characters +for code, _ in pairs(whitespaceCharacters) do + illegalCharacters[code] = true +end +-- include digit characters +for code, _ in pairs(digitCharacters) do + illegalCharacters[code] = true +end + function onload(saveState) -- Codenames script version @@ -263,6 +333,9 @@ function onload(saveState) redToken.interactable = false blueToken.interactable = false + -- Create variable for storing typed clues + currentEnteredClue = {} + -- Get the list of decks api_getDecks() @@ -498,8 +571,25 @@ function searchDecks(searchTerm) end function clueEntered(player, value) - if value:match("\n") then - local color = player.color + -- Because of how MoonSharp handles its matching expressions, + -- absolutely no expression matching from the string library can be used for unicode clues. + -- It converts all characters to values 0..255 before running the expression on it. + -- eg. %s will match all characters with codes 0x0A, 0x20, 0x10A, 0x400A, 0x7020 + -- string.lower upper find etc. work fine and properly even for other character sets + + local color = player.color + local newLineInd = string.find(value, "\n") + if newLineInd == nil then + -- Save the clue for when the user presses enter + currentEnteredClue[color] = value + else + -- Get the clue from before the user pressed enter + if currentEnteredClue[color] then + value = currentEnteredClue[color] + currentEnteredClue[color] = nil + else + value = "" + end -- Reset the text box local resetInput = { @@ -508,6 +598,11 @@ function clueEntered(player, value) } UI.setAttributes(color:lower() .. "ClueText", resetInput) + --if the clue is empty then do nothing + if #value == 0 then + return + end + -- if the game hasn't been started, a clue cannot be entered if gameState.status ~= 1 then Player[color].broadcast("[a020f0]» [da1918]ERROR: [ffffff]You must start a game to enter a clue! [a020f0]«") @@ -520,8 +615,11 @@ function clueEntered(player, value) return end - -- Remove the newline, trim the clue, and convert to lowercase - value = value:gsub("\n", ""):match("%s*(.-)%s*$"):lower() + -- if the clue is long, dont bother processing it + if #value > 50 then + Player[color].broadcast("[a020f0]» [da1918]ERROR: [ffffff]Invalid clue. Please enter a valid clue and push ENTER! [a020f0]«") + return + end -- Parse the entered clue into its respective parts local clue, number, error = getClueDetails(value) @@ -541,6 +639,9 @@ function clueEntered(player, value) return end + -- Standardize clue to lowercase + clue = clue:lower() + -- Don't allow a clue that isn't covered for cardIndex, cardData in ipairs(cards) do if not cardData.covered and cardData.value:lower() == clue:lower() then @@ -552,10 +653,10 @@ function clueEntered(player, value) -- Track remaining clues if number == "inf" then gameState.guessesLeft = -1 - elseif number == "0" then + elseif tonumber(number) == 0 then gameState.guessesLeft = -1 else - gameState.guessesLeft = tostring(number) + 1 + gameState.guessesLeft = tonumber(number) + 1 end -- Encode the finished clue @@ -564,99 +665,143 @@ function clueEntered(player, value) -- Enable voting for the current team gameState.canVote = true - -- Send analytics data for the a new clue + -- Send analytics data for the new clue api_newClue(clue, (number == "inf" and -1 or number), Player[color].steam_id) end end function getClueDetails(processedClue) - -- How many hyphens are there? - local clue, number - local _, hyphenCount = string.gsub(processedClue, "%-", "") - local _, spaceCount = string.gsub(processedClue, "%s", "") - - if hyphenCount == 0 then - -- Single word with space (or no space) as delimiter - if spaceCount > 1 then - return nil, nil, true - end - - local checks = { - "^(%a+)(%s*)(%d+)$", - "^(%a+)(%s+)(inf)$" - } - - for _, check in ipairs(checks) do - local status, clue, _, number = pcall(function() return string.match(processedClue, check) end) - if status then - -- Parsing successful - check for nil values just in case - if clue != nil and number != nil then - -- Return the clue and number - return clue, number, false - end + local clueState = {} + clueState.PRE_WHITESPACE = 1 + clueState.INF_N = 2 + clueState.INF_I = 3 + clueState.NUMBER = 4 + clueState.INF_WHITESPACE = 5 -- inf must have 1 space before clue + clueState.PRE_CLUE_WHITESPACE = 6 -- this state allows for 1 hyphen + clueState.CLUE = 7 -- the clue allows 1 hyphen + clueState.CLUE_ON_HYPHEN = 8 + clueState.POST_WHITESPACE = 9 + + local clue = "" + local number = "" + + local invalid = false + local state = clueState.PRE_WHITESPACE + local hyphenCount = 0 + -- process clue backwards as it is easier + for ind = #processedClue, 1, -1 do + -- This state machine will detect an invalid clue and stop processing if it + -- finds an invalid character before it reaches the beginning of the input + local ch = string.sub(processedClue,ind,ind) + local code = string.unicode(ch) + if state == clueState.PRE_WHITESPACE then + if string.lower(ch) == "f" then + state = clueState.INF_N + number = "inf" + elseif digitCharacters[code] then + state = clueState.NUMBER + number = digitCharacters[code]..number + elseif whitespaceCharacters[code] then + -- continue + else + invalid = true end - end - - -- No valid clues detected - return nil, nil, true - - elseif hyphenCount == 1 then - -- Either a hypenated word with a space (or no space) as delimiter - -- or a single word with a hyphen (and possibly spaces) as delimiter - if spaceCount > 2 then - return nil, nil, true - end - - local checks = { - "^(%a+%-%a+)(%s*)(%d+)$", - "^(%a+%-%a+)(%s+)(inf)$", - "^(%a+)(%s*%-%s*)(%d+)$", - "^(%a+)(%s*%-%s*)(inf)$" - } - - for _, check in ipairs(checks) do - local status, clue, _, number = pcall(function() return string.match(processedClue, check) end) - if status then - -- Parsing successful - check for nil values just in case - if clue != nil and number != nil then - -- Return the clue and number - return clue, number, false + elseif state == clueState.INF_N then + if string.lower(ch) == "n" then + state = clueState.INF_I + else + invalid = true + end + elseif state == clueState.INF_I then + if string.lower(ch) == "i" then + state = clueState.INF_WHITESPACE + else + invalid = true + end + elseif state == clueState.NUMBER then + if digitCharacters[code] then + number = digitCharacters[code]..number + elseif whitespaceCharacters[code] then + state = clueState.PRE_CLUE_WHITESPACE + elseif hyphenCharacters[code] then + state = clueState.PRE_CLUE_WHITESPACE + hyphenCount = hyphenCount + 1 + elseif illegalCharacters[code] then + invalid = true + else + state = clueState.CLUE + hyphenCount = 0 + clue = ch..clue + end + elseif state == clueState.INF_WHITESPACE then + if whitespaceCharacters[code] then + state = clueState.PRE_CLUE_WHITESPACE + elseif hyphenCharacters[code] then + state = clueState.PRE_CLUE_WHITESPACE + hyphenCount = hyphenCount + 1 + else + invalid = true + end + elseif state == clueState.PRE_CLUE_WHITESPACE then + if whitespaceCharacters[code] then + -- continue + elseif hyphenCharacters[code] and hyphenCount < 1 then + hyphenCount = hyphenCount + 1 + elseif illegalCharacters[code] then + invalid = true + else + state = clueState.CLUE + hyphenCount = 0 + clue = ch..clue + end + elseif state == clueState.CLUE or state == clueState.CLUE_ON_HYPHEN then + if whitespaceCharacters[code] then + if state == clueState.CLUE_ON_HYPHEN then + invalid = true + end + state = clueState.POST_WHITESPACE + elseif hyphenCharacters[code] then + state = clueState.CLUE_ON_HYPHEN + hyphenCount = hyphenCount + 1 + if hyphenCount > 1 then + invalid = true end + clue = ch..clue + elseif illegalCharacters[code] then + invalid = true + else + state = clueState.CLUE + clue = ch..clue + end + elseif state == clueState.POST_WHITESPACE then + if whitespaceCharacters[code] then + -- continue + else + invalid = true end + else + invalid = true -- we should never reach here end - -- No valid clues detected - return nil, nil, true - - elseif hyphenCount == 2 then - - if spaceCount > 2 then + if invalid then + -- This is an invalid clue + -- print("c:", clue, " n:", number, " i:", ind, " ch:", ch, " code:", code, " state:", state) return nil, nil, true end + end - local checks = { - "^(%a+%-%a+)(%s*%-%s*)(%d+)$", - "^(%a+%-%a+)(%s*%-%s*)(inf)$" - } - - for _, check in ipairs(checks) do - local status, clue, _, number = pcall(function() return string.match(processedClue, check) end) - if status then - -- Parsing successful - check for nil values just in case - if clue != nil and number != nil then - -- Return the clue and number - return clue, number, false - end - end - end - - -- No valid clues detected + if not (state == clueState.CLUE or state == clueState.POST_WHITESPACE) or clue == "" then + -- We either ended on a hyphen or did not receive a parsable clue return nil, nil, true + end - else - -- Clue has too many hyphens - return nil, nil, true + -- Clean number value + if number != "inf" then + number = tostring(tonumber(number)) end + + -- Return the clue and number + return clue, number, false end function rotateclues() @@ -1621,7 +1766,7 @@ function processChat(message) end end - return command, args:gsub("^%s*(.-)%s*$", "%1") + return command, args:gsub("^%s*(.*%S+)%s*$", "%1"):gsub("^%s+$","") end function toggleTurns()