Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding unicode friendly clue parsing #92

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
313 changes: 229 additions & 84 deletions src/Global.-1.ttslua
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,76 @@ analytics =
sessions = {}
}

----------[ Character sets ]----------

digits_table = {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these split up into 10 tables?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The digits_table[0] table lists all the characters that are the number 0 in other languages.
The digits_table[1] table lists all the characters that are the number 1 in other languages.
etc.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by that?

[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
Expand Down Expand Up @@ -263,6 +333,9 @@ function onload(saveState)
redToken.interactable = false
blueToken.interactable = false

-- Create variable for storing typed clues
currentEnteredClue = {}
Ryan6578 marked this conversation as resolved.
Show resolved Hide resolved

-- Get the list of decks
api_getDecks()

Expand Down Expand Up @@ -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 = {
Expand All @@ -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

Ryan6578 marked this conversation as resolved.
Show resolved Hide resolved
-- 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]«")
Expand All @@ -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)
Ryan6578 marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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))
Ryan6578 marked this conversation as resolved.
Show resolved Hide resolved
end

-- Return the clue and number
return clue, number, false
end

function rotateclues()
Expand Down Expand Up @@ -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()
Expand Down