I'm working on a word game, and I'm trying to dynamically generate puzzles based on arrays of random letters from a weighted set. What I have below works, but the word list I'm using is fairly limited (about 2300 words), and it takes forever. My last test went through over 1300 groups of possible words before it found one contained 5 matching words from the word list. I've tested it with a larger word set of 5700+ words, and can find a puzzle in an acceptable time frame.
The Question:
Is there a way to spin off several calls to Puzzle.generate()
and return the result of the first one to finish? I've thought about placing several in a TaskGroup and then canceling when a result is returned, but the while
loop makes it difficult to cancel the other tasks.
Here's psudo code for what I'd like to be able to do...
func multiGenerate() async -> Puzzle {
// Maybe this needs to be withThrowingTaskGroup ?
return await withTaskGroup(of: Puzzle.self, body: { group in
var puzzle: Puzzle!
for _ in 0..<5 {
group.addTask {
// try Task.checkCancellation()
return await Puzzle.generate()
}
}
for await result in group {
group.cancelAll()
puzzle = result
}
return puzzle
})
}
...and here's what I'm using to generate the puzzles
struct Puzzle {
let outerLetters: [String]
let middleLetters: [String]
let centerLetter: [String]
let words: [String]
var found = [String]()
static func generate() async -> Self {
var outerLetters: [String]?
var middleLetters: [String]?
var centerLetter: [String]?
var words: [String]?
var count: Int = 0
// If one of the other calls to `generate()` returns a result, how do I break out of this and cancel any remaining tasks?
var finished = false
while !finished {
count += 1
let outer = ContentView.ViewModel.randomizeAvailableLetters(tileArraySize: 16, from: ContentView.ViewModel.weightedOuter)
let middle = ContentView.ViewModel.randomizeAvailableLetters(tileArraySize: 8, from: ContentView.ViewModel.weightedMiddle)
let center = ContentView.ViewModel.randomizeAvailableLetters(tileArraySize: 1)
let possibleWords: Set<String> = Set(await Puzzle.getWords(outer: outer, middle: middle, center: center))
var actual = [String]()
for word in possibleWords {
if ContentView.ViewModel.wordList.contains(word) {
actual.append(word)
}
}
finished = actual.count >= 5
if finished {
outerLetters = outer
middleLetters = middle
centerLetter = center
words = actual
print("Count: \(count)")
}
}
guard let outer = outerLetters, let middle = middleLetters, let center = centerLetter, let words = words else {
fatalError("You should not be here...")
}
return Puzzle(outerLetters: outer, middleLetters: middle, centerLetter: center, words: words)
}
static func getWords(outer: [String], middle: [String], center: [String]) async -> [String] {
@Sendable func getOuter(with middle: [String], outer: [String]) async -> [String] {
var possibleWords = [String]()
// Construct some words...
let result = await withTaskGroup(of: [String].self) { group -> [String] in
var words = [String]()
for i in 0..<middle.count {
group.addTask {
return getMiddle(with: middle, outer: outer)
}
}
for await word in group {
words.append(contentsOf: word)
}
return words
}
possibleWords.append(contentsOf: result)
return possibleWords
}
@Sendable func getMiddle(with middle: [String], outer: [String]) -> [String] {
var possibleWords = [String]()
// Construct some more words...
return possibleWords
}
return await withTaskGroup(of: [String].self) { group in
var possibleWords = [String]()
for i in 0..<outer.count {
group.addTask {
return await getOuter(with: middle, outer: outer)
}
}
for await words in group {
possibleWords.append(contentsOf: words)
}
return possibleWords
}
}
}