0

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
            }
            
        }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
forgot
  • 2,160
  • 2
  • 19
  • 20
  • Have you used profiling to determine where the time is being spent? The task you have described should be completed in a very short amount of time if you use the right data structure. – Paulw11 Jun 15 '22 at 05:47
  • Not yet, but I'll be taking a look in the morning. The data structure seems to be correct since it works quickly on a larger word list. With a smaller list, and randomly generated letters, it has to iterate a lot more to get a hit. – forgot Jun 15 '22 at 05:51
  • Are you using something like a [DAFSA](https://en.wikipedia.org/wiki/Deterministic_acyclic_finite_state_automaton) ? – Paulw11 Jun 15 '22 at 05:51
  • I'm unfamiliar with that type of data structure, but I'll do some research. I'm assuming that would speed up the `wordList.contains(word)` check? If so, that's definitely helpful. – forgot Jun 15 '22 at 05:56
  • Each pass through the `while` loop generates about 960 possible combinations (words can only be selected in specific ways, so there's no need to get every possible combination), and then it checks those against the word list. Not sure if that's helpful information or not. – forgot Jun 15 '22 at 05:58
  • You might also be able to use Combine instead of async await. You could set up a publisher using `merge(with:,,,,,)` chained to `output(at:0)` to get the first value and cancel the other publishers. – Paulw11 Jun 15 '22 at 06:03

0 Answers0