diff --git a/README.md b/README.md index 4dcf1d3..b12ac06 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Note: Although this is advertised as AI, it is not. It's simply a semi-optimized - [x] Make the app functional on wearable devices to improve ease-of-use ([watch](https://github.com/PeculiarProgrammer/Ghost-AI/tree/watch) branch) - [ ] Add more dictionary options - [ ] Implement tests -- [ ] Improve efficiency +- [x] Improve efficiency of solving algorithm +- [ ] Improve efficiency of `determinePercentage` and similar If you can think of any more, please leave an issue. diff --git a/lib/algorithm.dart b/lib/algorithm.dart index 2ab0d61..b7f2be6 100644 --- a/lib/algorithm.dart +++ b/lib/algorithm.dart @@ -1,28 +1,32 @@ library evaluate; -import 'package:retrieval/trie.dart'; - final List letters = List.generate(26, (i) => String.fromCharCode(97 + i)); final Set lettersSet = Set.from(letters); -List recursiveCartesianProduct( - String path, int n, int depth, Trie dictionary) { +List recursiveCartesianProduct( + int n, int depth, TrieNode dictionary) { if (depth == 0) { - return [path]; + return [dictionary]; } - List combinations = []; + List combinations = []; for (var letter in letters) { - if ((dictionary.find(path + letter).isEmpty || - dictionary.has(path + letter)) && + var temporaryDictionary = dictionary.walk(letter); + + if (temporaryDictionary == null) { + continue; + } + + if ((temporaryDictionary.childrenCount == 0 || + temporaryDictionary.isEndOfWord) && depth > 1) { continue; } - combinations.addAll( - recursiveCartesianProduct(path + letter, n, depth - 1, dictionary)); + combinations + .addAll(recursiveCartesianProduct(n, depth - 1, temporaryDictionary)); } return combinations; @@ -37,39 +41,40 @@ int mex(Set s) { } dynamic evaluate( - int player, int playerCount, Trie dictionary, Map game, - {String path = ""}) { - if (dictionary.find(path).isEmpty) { + int player, int playerCount, TrieNode dictionary, Map game) { + if (dictionary.childrenCount == 0) { return null; } - if (dictionary.has(path)) { + if (dictionary.isEndOfWord) { return false; } else { Set chv = {}; for (var letter in lettersSet) { - if (dictionary.find(path + letter).isEmpty || - dictionary.has(path + letter)) { + var temporaryDictionary = dictionary.walk(letter); + + if (temporaryDictionary == null || + temporaryDictionary.childrenCount == 0 || + temporaryDictionary.isEndOfWord) { continue; } - if (path.length % playerCount == player) { - chv.add(evaluate(player, playerCount, dictionary, game, - path: path + letter)); + if (dictionary.currentLength % playerCount == player) { + chv.add(evaluate(player, playerCount, temporaryDictionary, game)); } else { int iterations = ((player % playerCount) - - path.length % playerCount + + dictionary.currentLength % playerCount + playerCount) % playerCount; // This solves path.length + iterations % playerCount == player for (var combination in recursiveCartesianProduct( - path + letter, iterations - 1, iterations - 1, dictionary)) { - chv.add(evaluate(player, playerCount, dictionary, game, - path: combination)); + iterations - 1, iterations - 1, temporaryDictionary)) { + chv.add(evaluate(player, playerCount, combination, game)); } } } + int answer = mex(chv); - game[path] = answer; + game[dictionary.currentWord] = answer; return answer; } @@ -101,3 +106,156 @@ double determinePercentage( return good / count; } + +// This is a slightly modified version of the trie implementation from retrieval (10x faster than the original for this use case) +class Trie { + final root = TrieNode(key: null, value: null); + + void insert(String word) { + var currentNode = root; + + var characters = word.split(""); + + for (int i = 0; i < characters.length; i++) { + currentNode = currentNode.putChildIfAbsent(characters[i], value: null); + currentNode.currentLength = i + 1; + currentNode.currentWord = word.substring(0, i + 1); + } + + currentNode.isEndOfWord = true; + } + + bool has(String word) { + return findPrefix(word, fromNode: root)?.isEndOfWord ?? false; + } + + bool hasChildren(String word) { + final prefix = findPrefix(word, fromNode: root); + + if (prefix == null) { + return false; + } + + return prefix.childrenCount > 0; + } + + List find(String prefix) { + final lastCharacterNode = findPrefix(prefix, fromNode: root); + + if (lastCharacterNode == null) { + return []; + } + + final stack = <_PartialMatch>[ + _PartialMatch(node: lastCharacterNode, partialWord: prefix), + ]; + final foundWords = []; + + while (stack.isNotEmpty) { + final partialMatch = stack.removeLast(); + + if (partialMatch.node.isEndOfWord) { + foundWords.add(partialMatch.partialWord); + } + + for (final child in partialMatch.node.getChildren()) { + stack.add( + _PartialMatch( + node: child, + partialWord: "${partialMatch.partialWord}${child.key}", + ), + ); + } + } + + return foundWords; + } +} + +class _PartialMatch { + final TrieNode node; + final String partialWord; + + _PartialMatch({ + required this.node, + required this.partialWord, + }); + + @override + String toString() => '_PartialMatch(node: $node, prefix: $partialWord)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is _PartialMatch && + other.node == node && + other.partialWord == partialWord; + } + + @override + int get hashCode => node.hashCode ^ partialWord.hashCode; +} + +class TrieNode { + final String? key; + + T? value; + + int currentLength = 0; + + String currentWord = ""; + + bool isEndOfWord = false; + + bool get isRoot => key == null; + + final Map> _children = {}; + + bool get hasChildren => _children.isEmpty; + + int get childrenCount => _children.length; + + TrieNode({required this.key, this.value}); + + Iterable> getChildren() { + return _children.values; + } + + bool hasChild(String key) { + return _children.containsKey(key); + } + + TrieNode? getChild(String key) { + return _children[key]; + } + + TrieNode putChildIfAbsent(String key, {T? value}) { + return _children.putIfAbsent( + key, + () => TrieNode(key: key, value: value), + ); + } + + TrieNode? walk(String key) { + return findPrefix(key, fromNode: this); + } + + @override + String toString() { + return "TrieNode(key=$key, value=$value)"; + } +} + +TrieNode? findPrefix(String prefix, {required TrieNode fromNode}) { + TrieNode? currentNode = fromNode; + + for (final character in prefix.split("")) { + currentNode = currentNode?.getChild(character); + if (currentNode == null) { + return null; + } + } + + return currentNode; +} diff --git a/lib/main.dart b/lib/main.dart index 095c808..35284fc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,7 +3,6 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:retrieval/trie.dart'; import "./algorithm.dart"; import "./data/frequency.dart"; import 'data/full_scrabble.dart'; @@ -234,7 +233,7 @@ class _MyHomePageState extends State { Map game = {}; - evaluate(arguments[1], arguments[2], dictionary, game); + evaluate(arguments[1], arguments[2], dictionary.root, game); sendPort.send([game, dictionary]); }, [receivePort.sendPort, player, playerCount, dictionaryType]); diff --git a/pubspec.lock b/pubspec.lock index 955c19a..e37199b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -139,14 +139,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" - retrieval: - dependency: "direct main" - description: - name: retrieval - sha256: b8fe753d97f2728a513d0e48a240cfe8fff9666523ba6b78da4fa7aa32c805d7 - url: "https://pub.dev" - source: hosted - version: "1.0.1" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 462c969..214f44e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,6 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 - retrieval: ^1.0.1 dev_dependencies: flutter_test: