Bingo script is filling in repeat numbers

I have made a script that fills in an InDesign file bingo card to generate as many as requested.
It works almost flawlessly. However, there is one part that perplexes me. The problem is obviously in the core of the script, where the numbers get chosen and then supposedly culled from the list of available choices. However, I get repeats of numbers, usually the final item in each list can get two or sometimes three repeats.

The code is below. If I have missed something or if there is just some weird memory issue happening, I would love to know what it is, and how to correct it. Thanks so much!

set textItems to {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75"}

set BItems to items 1 thru 15 of textItems
set IItems to items 16 thru 30 of textItems
set NItems to items 31 thru 45 of textItems
set GItems to items 46 thru 60 of textItems
set OItems to items 61 thru 75 of textItems


set ThisPlace to (choose folder with prompt "Where do you want to save your bingo cards?")
--return ThisPlace


set BatchNumber to (display dialog "How many bingo cards do you want?" default answer "1")
set BatchRound to text returned of the result --to get the number of bingo cards being generated
--return BatchRound

tell application "Adobe InDesign 2025"
	set user interaction level of script preferences to never interact --This allows the document to revert without requiring a dialog to OK
end tell

repeat with u from 1 to BatchRound
	set ThisRun to u as string
	--return ThisRun
	
	tell application "Adobe InDesign 2025"
		
		set TheName to the name of document 1
		--return TheName
		my CullName(TheName, ThisRun, ThisPlace) --this gets rid of the " Template.indd" part of the file name and builds the full path for each file
		set ThisDoc to result
		--return ThisDoc
		
		
		tell document 1
			set theTextLabels to the count of text frames
			set TextFrameNames to the id of text frames
			--return TextFrameNames
			--return theTextLabels
			set textFrames to the label of text frames
			--return textFrames
			set BankOne to (the text frames whose label contains "B")
			--return BankOne
			set BankTwo to (the text frames whose label contains "I")
			--return BankTwo
			set BankThree to (the text frames whose label contains "N")
			--return BankThree
			set BankFour to (the text frames whose label contains "G")
			set BankFive to (the text frames whose label contains "O")
			--return BankFive
			
			
			--Let's do each bank of text frames with numbers 
			
			set theBankz to {BankOne, BankTwo, BankThree, BankFour, BankFive}
			set ItemSetz to {BItems, IItems, NItems, GItems, OItems}
			
			repeat with b from 1 to 5
				
				set thisBank to item b of theBankz --this resets the sets of numbers before going through another card run
				--return thisBank
				set theseItemz to item b of ItemSetz
				--return theseItemz
				
				
				--below is the core of the script, but we need to go though each bank of text frames before moving on to the others
				repeat with i from 1 to (count of items of thisBank)
					
					set randNumb to random number from 1 to count of items of theseItemz --sets random number from bank for specified column
					--return randNumb
					set thePull to item randNumb of theseItemz -- we  need the specific item not it's count number
					
					set randFrame to item i of thisBank
					--return randFrame
					set (contents of randFrame) to thePull as string
					-- this works! Now we need to pull the used items out of the sets
					--return
					if randNumb is less than or equal to ((count of items of theseItemz) - i) then
						if randNumb is greater than 1 then
							set textItemsStart to items 1 thru (randNumb - 1) of theseItemz
							--return textBItemsStart
							--pull out the item we used
							set textItemsNext to items (randNumb + 1) thru end of theseItemz
							--return textBItemsNext
							set theseItemz to textItemsStart & textItemsNext
							--return BItems
						else if randNumb is 1 then
							set theseItemz to items 2 thru end of theseItemz
						end if
					end if
					--return BItems
					
					--return BankOne
				end repeat
				
			end repeat
			
			--return
		end tell
		save a copy document 1 to alias ThisDoc
		revert document 1
		
	end tell
end repeat

tell application "Adobe InDesign 2025"
	set user interaction level of script preferences to interact with all
end tell




on CullName(TheName, ThisRun, ThisPlace)
	set thisName to (characters 1 through ((offset of " Template.indd" in TheName) - 1) of TheName) as string
	set thePath to ThisPlace & thisName & "_" & ThisRun & ".indd" as string
	return thePath
end CullName

Also attached is my InDesign file to use for testing. Thanks!

Bingo Card Template.indd.zip (455.9 KB)

Hi.

It’s difficult to see quite what’s supposed to be happening without being familiar with InDesign. However:

Your variable randNum is a random number from 1 to (the number of items in theItemz) and thePull is item randNum of theItemz.

If randNumb is less than or equal to ((count of items of theseItemz) - (a number between 1 and the number of items in thisBank)) then thePull is removed from theseItemz. (More accurately, a new theseItemz is constructed which doesn’t contain thePull.)

But your code doesn’t provide for when randNum is greater than ((count of items of theseItemz) - (a number between 1 and the number of items in thisBank)), which obviously it can be sometimes. In these cases, thePull remains in the list and can be chosen again.

Thanks Nigel,

That is a good insight.
I went back to figure out how to make that happen, and this is what I came up with, and so far, after a run of 50 cards, I did not see any repeats.

		if randNumb is less than or equal to ((count of items of theseItemz) - 1) then
						if randNumb is greater than 1 then
							set textItemsStart to items 1 thru (randNumb - 1) of theseItemz
							--return textBItemsStart
							--pull out the item we used
							set textItemsNext to items (randNumb + 1) thru end of theseItemz
							--return textBItemsNext
							set theseItemz to textItemsStart & textItemsNext
							--return BItems
						else if randNumb is 1 then
							set theseItemz to items 2 thru end of theseItemz
							
						end if
					else if randNumb is equal to (count of items of theseItemz) then
						set textItemsStart to items 1 thru (randNumb - 1) of theseItemz
						set theseItemz to textItemsStart

So, the critical part, besides the line to include the incident of a number equal to the count of the items, was where to include it. I found the place is to have it as the second option in the if statement, otherwise, it still didn’t work.

Thanks!

I believe your duplicate number selection problem arises in some way from the line

if randNumb is less than or equal to ((count of items of theseItemz) - i) then

But I didn’t want to puzzle out that knot so here’s a simpler number selector and a couple different used-number remover methods. Look for the line “–Choose one of the following two lines to use. Comment the other one out or remove it.” currently it’s coded to use the vanilla AppleScript version. The alternate version uses ASOBJC. Either version produces 100 cards in just under a minute OMM.

use framework "Foundation"
use scripting additions

set textItems to {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75"}

set BItems to items 1 thru 15 of textItems
set IItems to items 16 thru 30 of textItems
set NItems to items 31 thru 45 of textItems
set GItems to items 46 thru 60 of textItems
set OItems to items 61 thru 75 of textItems

set ThisPlace to (choose folder with prompt "Where do you want to save your bingo cards?")

set BatchNumber to (display dialog "How many bingo cards do you want?" default answer "1")
set BatchRound to text returned of the result --to get the number of bingo cards being generated

tell application "Adobe InDesign 2025"
	set user interaction level of script preferences to never interact --This allows the document to revert without requiring a dialog to OK
end tell

repeat with u from 1 to BatchRound
	set ThisRun to u as string
	tell application "Adobe InDesign 2025"
		set TheName to the name of document 1
		my CullName(TheName, ThisRun, ThisPlace) --this gets rid of the " Template.indd" part of the file name and builds the full path for each file
		set ThisDoc to result
		tell document 1
			set theTextLabels to the count of text frames
			set TextFrameNames to the id of text frames
			set textFrames to the label of text frames
			set BankOne to (the text frames whose label contains "B")
			set BankTwo to (the text frames whose label contains "I")
			set BankThree to (the text frames whose label contains "N")
			set BankFour to (the text frames whose label contains "G")
			set BankFive to (the text frames whose label contains "O")
			--Let's do each bank of text frames with numbers 
			set theBankz to {BankOne, BankTwo, BankThree, BankFour, BankFive}
			set ItemSetz to {BItems, IItems, NItems, GItems, OItems}
			repeat with b from 1 to 5
				set thisBank to item b of theBankz --this resets the sets of numbers before going through another card run
				set theseItemz to item b of ItemSetz
				--below is the core of the script, but we need to go though each bank of text frames before moving on to the others
				repeat with i from 1 to (count of items of thisBank)
					set randNumb to some item of items of theseItemz
					--Choose one of the following two lines to use. Comment the other one out or remove it.
					set theseItemz to my purgeUsedNumbers(randNumb, theseItemz)
					--set theseItemz to my Array_Remove_Values_From_List(theseItemz, {randNumb})
					set randFrame to item i of thisBank
					set (contents of randFrame) to randNumb as string
				end repeat
			end repeat
		end tell
		save a copy document 1 to alias ThisDoc
		revert document 1
	end tell
end repeat

tell application "Adobe InDesign 2025"
	set user interaction level of script preferences to interact with all
end tell

on purgeUsedNumbers(randNumb, theseItemz)
	set AppleScript's text item delimiters to "*"
	set theseItemz to theseItemz as text
	set theseItemz to "*" & ((theseItemz) as text) & "*"
	set AppleScript's text item delimiters to "*" & randNumb & "*"
	set theseItemz to text items of theseItemz
	set AppleScript's text item delimiters to "*"
	set theseItemz to theseItemz as text
	set AppleScript's text item delimiters to "*"
	set theseItemz to text items 2 thru -2 of theseItemz
	set AppleScript's text item delimiters to ""
	return theseItemz
end purgeUsedNumbers

on Array_Remove_Values_From_List(inputList, valuesToRemove)
	try
		set theNSArray to (current application's NSMutableArray's arrayWithArray:inputList)
		set theValuesToRemoveArray to my (NSArray's arrayWithArray:valuesToRemove)
		theNSArray's removeObjectsInArray:theValuesToRemoveArray
		return theNSArray as list
	on error errorText number errornumber partial result errorResults from errorObject to errorExpectedType
		error "<Array_Remove_Values_From_List>" & errorText number errornumber partial result errorResults from errorObject to errorExpectedType
	end try
end Array_Remove_Values_From_List


on CullName(TheName, ThisRun, ThisPlace)
	set thisName to (characters 1 through ((offset of " Template.indd" in TheName) - 1) of TheName) as string
	set thePath to ThisPlace & thisName & "_" & ThisRun & ".indd" as string
	return thePath
end CullName

Thanks, paulskinner!

I’ll give it a shot.

@T_Rex if you’re interested, here’s where I wound up with this Bingo code. This version is terse, limits the code addressing InDesign as much as possible, retains the chosen folder and card counts, and also populates the center “Free Space” square. You can comment out that line with no other impact on it. I don’t modify the template name on saving, I just iterate the saved card number but you already have code for that.

use framework "Foundation"
use scripting additions
property previousOutputFolder : path to desktop folder as alias
property previoustotalCardsDesired : 100

set outputFolder to (choose folder with prompt "Where do you want to save your bingo cards?" default location previousOutputFolder)
set previousOutputFolder to outputFolder
set totalCardsDesired to text returned of (display dialog "How many bingo cards do you want?" default answer previoustotalCardsDesired)
set previoustotalCardsDesired to totalCardsDesired

tell application "Adobe InDesign 2025" to set user interaction level of script preferences to never interact --This allows the document to revert without requiring a dialog to OK
repeat with cardIndex from 1 to totalCardsDesired
	set theColumnValueList to {{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"}, {"16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"}, {"31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45"}, {"46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "60"}, {"61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75"}}
	tell application "Adobe InDesign 2025" to tell document 1 to set theTextFrameList to {the text frames whose label contains "B", the text frames whose label contains "I", the text frames whose label contains "N", the text frames whose label contains "G", the text frames whose label contains "O"}
	set thisValueList to item 1 of theColumnValueList
	repeat with thisColumnIndex from length of theTextFrameList to 1 by -1
		set thisColumnsTextFrames to item thisColumnIndex of theTextFrameList
		set thisValueList to item -1 of theColumnValueList
		repeat with textFrameIndex from 1 to length of thisColumnsTextFrames
			set thisTextFrame to item textFrameIndex of thisColumnsTextFrames
			set chosenValue to some item of thisValueList
			set thisValueList to my Array_Remove_Values_From_List(thisValueList, {chosenValue})
			tell application "Adobe InDesign 2025" to set contents of thisTextFrame to chosenValue
		end repeat
		if length of theColumnValueList > 1 then set theColumnValueList to items 1 thru -2 of theColumnValueList
	end repeat
	tell application "Adobe InDesign 2025"
		set (contents of (item 3 of item 3 of theTextFrameList)) to "Free Space"
		save a copy document 1 to alias (outputFolder & "BingoCard" & "_" & cardIndex & ".indd" as string)
	end tell
end repeat
tell application "Adobe InDesign 2025"
	revert document 1
	set user interaction level of script preferences to interact with all
end tell

on Array_Remove_Values_From_List(inputList, valuesToRemove)
	try
		set theNSArray to (current application's NSMutableArray's arrayWithArray:inputList)
		set theValuesToRemoveArray to my (NSArray's arrayWithArray:valuesToRemove)
		theNSArray's removeObjectsInArray:theValuesToRemoveArray
		return theNSArray as list
	on error errorText number errornumber partial result errorResults from errorObject to errorExpectedType
		error "<Array_Remove_Values_From_List>" & errorText number errornumber partial result errorResults from errorObject to errorExpectedType
	end try
end Array_Remove_Values_From_List

And from a previous Bingo-centric post I have this Bingo Caller script that seems appropriate to include. It presents calls and once a bingo is possible it offers a “Bingo!” button in addition to the "“Call Again” button. This presents a B-I-N-G-O sorted calls list to check the possible bingo against. If it’s not a valid bingo you can resume calling. This code supports the “Free Space” style cards but comments include a line for cards without this.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "GameplayKit"
use scripting additions

set randomSource to current application's GKARC4RandomSource's new()
set BingoNumbers to {}
repeat with i from 1 to 75
	set the end of BingoNumbers to (item (((i + 14) div 15)) of {"B - ", "I - ", "N - ", "G - ", "O - "}) & i
end repeat
set bingoCalls to (randomSource's arrayByShufflingObjectsInArray:BingoNumbers) as list
set calledLetters to {}
set buttonOptionList to {"Call Again"}
repeat with i from 1 to length of bingoCalls
	set thisBingoCall to (item i of bingoCalls)
	set thisCalledLetter to (character 1 of thisBingoCall)
	if calledLetters does not contain thisCalledLetter then set the end of calledLetters to thisCalledLetter
	if i ≥ 4 and calledLetters contains "B" and calledLetters contains "I" and calledLetters contains "G" and calledLetters contains "O" then set buttonOptionList to {"BINGO!", "Call Again"}
	--No Free Space? Uncomment the line below and comment the line above.
	--	if i ≥5  and calledLetters contains "B" and calledLetters contains "I" and calledLetters contains "N" and calledLetters contains "G" and calledLetters contains "O" then set buttonOptionList to {"BINGO!", "Call Again"}
	set bingoCallResults to button returned of (display dialog return & return & tab & tab & tab & i & tab & tab & tab & item i of bingoCalls & return & return buttons buttonOptionList default button "Call Again")
	if bingoCallResults is "BINGO!" and confirmClaimedBingo(i, bingoCalls) is true then exit repeat
end repeat
display dialog "We have a winner!"


on confirmClaimedBingo(i, bingoCalls)
	set sortedBingoCalls to {}
	repeat with thisLetterIndex from 1 to 5
		set thisLetter to item thisLetterIndex of {"B", "I", "N", "G", "O"}
		repeat with thisBingoCallIndex from 1 to i
			set thisBingoCall to item thisBingoCallIndex of bingoCalls
			if character 1 of thisBingoCall is thisLetter then
				set the end of sortedBingoCalls to thisBingoCall
			end if
		end repeat
	end repeat
	set AppleScript's text item delimiters to return
	if button returned of (display dialog "Called bingo numbers:" & return & return & sortedBingoCalls as text buttons {"We have a winner!", "No winner yet"} default button "No winner yet") is "We have a winner!" then return true
	return false
end confirmClaimedBingo