Help with hint system

I’m trying to make a hint system for my guessing game but I am having trouble generating the hint.

I’m trying to convert RainbowDash to R____o____h but I can’t figure out how I would replace the inside characters with underscores.

I can get the first, middle, and last character already.

local characters = workspace.Map.Characters:GetDescendants()

local hintLabel = script.Parent.HintLabel

local player = game.Players.LocalPlayer
local stage = player.leaderstats.Stage

function generateHint(stageValue)
	for _, object in ipairs(characters) do
		if object:IsA("Model") then
			local pony = object.Name:split("_")
			local ponyName = pony[1] -- example: RainbowDash
			local ponyStage = tonumber(pony[2])
			
			if stageValue ~= ponyStage then continue end
			
			-- Get first, middle, and last character from name and replace others with spaces
			
			local firstCharacter = string.sub(ponyName, 1, 1) -- example: R
			local lastCharacter = string.sub(ponyName, string.len(ponyName), string.len(ponyName)) -- example: h
			local middleCharacter = string.sub(ponyName, tonumber(math.round(string.len(ponyName) / 2)), tonumber(math.round(string.len(ponyName) / 2))) -- example: o
			print(firstCharacter.." "..middleCharacter.." "..lastCharacter) -- example: R o h
			
			local generatedHint = -- help here
			print(generatedHint)
		end	
	end
end

stage:GetPropertyChangedSignal("Value"):Connect(function()
	generateHint(stage.Value)
end)
1 Like

I can’t help but suggest adding a little bit more modularity to your hint generation. Try incorporating “reveal” arguments that specify which letters to reveal in case you want specific or random characters past the first, middle, and last ones:

function toHint(str: string, ...: number): string -- one way to specify "reveal" positions are with tuples, but a table works exactly the same here and may even be more readable
	local reveal = {}; -- dictionary for speedy checks to see which characters to reveal
	local hint = ''; -- we add to this string to eventually yield our end product
	
	for _, i in {...} do -- convert the tuple to a table and iterate through all reveal indices we specified
		reveal[math.ceil(i)] = true; -- add the index to our dictionary as a key to hasten lookups, thereby reducing runtime complexity (my other alternative was a nested for-loop which can get messy for large strings)
        -- additionally, we round the index up to the nearest whole integer so that the next for loop correctly perceives it
	end
	
	for i = 1, #str do -- from i = 1 to the string's length
		hint ..= reveal[i] and str:sub(i, i) or '_'; -- append the next character to our hint if its index is in the reveal table, otherwise just add an underscore
	end
	
	return hint;
end

This function should be pretty simple and seamless for your use case. I don’t recommend getting vital information from instance names, but this is how you would probably go about it while preserving your original code:

local answer = object.Name:split'_'[1];
local generatedHint = toHint(answer, 1, #answer / 2, #answer); -- reveal the first character (1), the middle character (#answer / 2 ; the value rounds up for us), and the last character (#answer ; just the string's length)

You can also get creative and randomly reveal characters in addition to the first and last:

local random = Random.new();

local answer = 'rainbowdash';
local generatedHint = toHint(answer, 1, random:NextInteger(2, #answer - 1), #answer);
1 Like

Another (possibly even more) performant solution is to add however many underscores are necessary before each revealed character:

function toHint(str: string, ...: number): string
	local reveal = {...};
	local hint = '';
	
	table.insert(reveal, #str + 1); -- go 1 beyond the string's length to allow the for loop to add necessary underscores between the largest reveal index and the end of the string
	table.sort(reveal); -- sort the reveal table so that the reveal indices are in order; allows the for-loop to calculate the distance between two indices correctly
	
	for i, pos in reveal do
		pos = math.ceil(pos);
		hint ..= ('_'):rep(pos - (reveal[i - 1] or 0) - 1) .. str:sub(pos, pos);
	end
	
	return hint;
end
1 Like

Some solid ideas above me ^^

My idea was just (in the case of having static hints), simply doing gsub, which you can still workaround if you want hints to slowly reveal like scribbl.io.

local letters = {"a", "b", "c"}
local text = "Happy bird chewing on a sock"

text = text:gsub(`[^%s%{letters}]`, "_")

Kinda forgot my string patterns so I apologize if I get anything wrong ^^', also I’m on phone so I can’t exactly test this out, but I believe this should substitute every character except the letters in the table and whitespace with an underscore.

1 Like

This code made it work great. I did forget one thing though. Some characters have 2 words in their name. Is there a way we can add a space in between the 2 words? Right now this is what it shows:

It should show Ra___o_ ___h

local characters = workspace.Map.Characters:GetDescendants()

local hintLabel = script.Parent.HintLabel

local player = game.Players.LocalPlayer
local stage = player.leaderstats.Stage

function toHint(str: string, ...: number): string
	local reveal = {...};
	local hint = '';

	table.insert(reveal, #str + 1); -- go 1 beyond the string's length to allow the for loop to add necessary underscores between the largest reveal index and the end of the string
	table.sort(reveal); -- sort the reveal table so that the reveal indices are in order; allows the for-loop to calculate the distance between two indices correctly

	for i, pos in reveal do
		pos = math.ceil(pos);
		hint ..= ('_'):rep(pos - (reveal[i - 1] or 0) - 1) .. str:sub(pos, pos);
	end

	return hint;
end

function showHint()
	for _, object in ipairs(characters) do
		if object:IsA("Model") then
			local pony = object.Name:split("_")
			local ponyName = pony[1] -- example: RainbowDash
			local ponyStage = tonumber(pony[2])

			if stage.Value ~= ponyStage then continue end
			
			local firstCharacter = 1
			local middleCharacter = ponyName:len() / 2
			local lastCharacter = ponyName:len()
			
			hintLabel.Text = toHint(ponyName, firstCharacter, 2, middleCharacter, lastCharacter)
		end
	end
end

showHint()

stage:GetPropertyChangedSignal("Value"):Connect(showHint)

I made the post without thinking of this my bad

heres some function which i believe you are in need of?

local word = "hello world"
local split, hidden = ConstructString(string.split(word, ""), "_")
-- adds a hint, position will be random if not provided
function AddHint(Position: number?)
	if (typeof(Position) ~= "number" then
		Position = math.random(#split)
	end

	local letter = string.sub(word, position, position)
	split[Position] = letter
	split, hidden = ConstructString(split)

	return true
end

-- removes a hint, position is required
function RemoveHint(Position: number)
	if (typeof(Position) ~= "number" then
		return false
	end

	local letter = string.sub(word, position, position)
	split[Position] = letter
	split, hidden = ConstructString(split)

	return true
end
-- contructs string, replaces all letters with Replace if provided
function ConstructString(Table: {}, Replace: string?)
	local Product = ""
	for i,v in Table do
		Product = `{Product}{Replace or v}`
		Table[i] = v
	end

	return Table, Product
end

lmk if anything is broken i didnt code this in studio

1 Like

You can do this by replacing only alphanumeric characters with an underscore (as opposed to concatenating repeated underscores together). Since my first method iterates through each and every character via for i = 1, #str do, it would be easy to determine whether str:sub(i, i) is whitespace or punctuation using match. Otherwise, you can use gsub in the second approach (the one I called possibly more performant) like this:

function toHint(str: string, ...: number): string
	local reveal = {...};
	local hint = '';

	table.insert(reveal, #str + 1);
	table.sort(reveal);

	for i, pos in reveal do
		pos = math.ceil(pos);
		hint ..= str:sub((reveal[i - 1] or 0) + 1, pos - 1):gsub('%w', '_') .. str:sub(pos, pos);
	end

	return hint;
end

Here, we create substrings between all desired reveal indices, then replace every alphanumeric character with an underscore. In this way, punctuation and whitespace are ignored in the final hint product.

I should also note that indices in this approach don’t ignore characters that the function did not replace, meaning #string / 2 could refer to whitespace or punctuation, and nothing would happen.

1 Like

I know this is a little late but how would I add a way to reveal one letter and it can stack.

An example would be if we had R______ H and we could reveal one random letter so it could turn into R_i_ H and you can continue revealing random letters like R_in ___H until the pony’s name is fully revealed.

This is my code currently:

local replicatedStorage = game:GetService("ReplicatedStorage")

local characters = workspace.Map.Characters:GetDescendants()

local hintLabel = script.Parent.HintLabel

local player = game.Players.LocalPlayer
local stage = player.leaderstats.Stage

local fillHintRemoteEvent = replicatedStorage.RemoteEvents.FillHint

function toHint(str: string, ...: number): string
	local reveal = {...};
	local hint = '';

	table.insert(reveal, #str + 1);
	table.sort(reveal);

	for i, pos in reveal do
		pos = math.ceil(pos);
		hint ..= str:sub((reveal[i - 1] or 0) + 1, pos - 1):gsub('%w', '_') .. str:sub(pos, pos);
	end

	return hint;
end

function showHint()
	hintLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
	for _, object in ipairs(characters) do
		if object:IsA("Model") then
			local pony = object.Name:split("_")
			local ponyName = pony[1]
			local ponyStage = tonumber(pony[2])

			if stage.Value ~= ponyStage then continue end
			
			hintLabel.Text = toHint(ponyName, 1, ponyName:len())
			break
		end
	end
end

showHint()

stage:GetPropertyChangedSignal("Value"):Connect(function()
	showHint()
end)

function fillHint(ponyName: string)
	hintLabel.Text = ponyName
	hintLabel.TextColor3 = Color3.fromRGB(0, 255, 0)
	hintLabel:TweenSize(UDim2.new(0.245, 0, 0.079, 0), Enum.EasingDirection.Out, Enum.EasingStyle.Quart, .1, true)
	wait(.1)
	hintLabel:TweenSize(UDim2.new(0.245, 0, 0.049, 0), Enum.EasingDirection.Out, Enum.EasingStyle.Quart, .1, true)
end

fillHintRemoteEvent.OnClientEvent:Connect(fillHint)

An iterator may provide the most readability and modularity if you want to reveal characters sequentially. In addition, I’ve decided to incorporate a second argument that streamlines the sequence’s determination; toHint() can internally provide your string’s alphanumeric length to a custom ‘selector’ function that dynamically returns any order of character positions (as in {number}). I implement our selector like this:

local rand = Random.new();

local function shuffleSelect(len: number): { number }	-- note that 'len' will be the passed string's alphanumeric length
	local order = {};
	
	for i = 1, len - 2 do	-- for every possible alphanumeric character position (minus 2)
		order[i] = i + 1;	-- populate order excluding positions '1' and 'len'
	end
	
	for i = #order, 2, -1 do	-- popular shuffling algorithm (fisher-yates)
		local j = rand:NextInteger(1, i);
		order[i], order[j] = order[j], order[i];
	end
	
	table.insert(order, 1, 1);	-- force the first character to be revealed first
	table.insert(order, 2, len);	-- force the last character to be revealed second
	
	return order;	-- push our final desired order to toHint()
end

Our new toHint() will accept both the “hint” string and our custom selector function:

local function toHint(
	str: string,
	selector: (number) -> { number }
)
	local order = selector(#str:gsub('%W', ''));	-- ask the selector to generate order given the string's alphanumeric length
	local hint = str:gsub('%w', '_');	-- generate a completely hidden hint
	local i = 0;	-- current position in reveal sequence (for iteration)

	return function()
		if i > #order then return; end	-- return null (i.e., terminate the for-loop) when all requested characters in 'order' reveal
		i += 1;
		
		local j = 0;	-- position in string WITHOUT whitespace or punctuation
		
		for k = 1, #str do	-- for every character in the string
			local char = str:sub(k, k);
			j += char:match'%w' and 1 or 0;	-- increment 'j' if the current character is alphanumeric
			
			if j ~= order[i] then continue; end	-- escape this iteration if this position isnt the next one to reveal
			
			hint = hint:sub(1, k - 1) .. char .. hint:sub(k + 1, #str);	-- if it is, then update the hint
		end
		
		return i, hint;	-- also return 'i' incase we need to know which iteration this is
	end
end

With these complex algorithms aside, we can simply integrate the selector (select(len)) with the iterator function (toHint(str, selector)), then use the iterator in a for-loop:

for i, hint in toHint('god\'s plan.', shuffleSelect) do
	print(i, hint);
end

This small loop shows that our selector properly reveals the first and last characters (in order) and the remaining ones at random:
image

edit: small optimizations and typos, also forgot to declare rand as a random number generator

2 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.