-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Expand file tree
/
Copy pathgame-code.dart
More file actions
217 lines (176 loc) · 6.09 KB
/
game-code.dart
File metadata and controls
217 lines (176 loc) · 6.09 KB
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import 'dart:collection';
import 'dart:math';
const List<String> allLegalGuesses = [...legalWords, ...legalGuesses];
const int defaultNumGuesses = 5;
enum HitType { none, hit, partial, miss, removed }
typedef Letter = ({String char, HitType type});
const List<String> legalWords = ['aback', 'abase', 'abate', 'abbey', 'abbot'];
/// Legal guesses minus legal wordles
const List<String> legalGuesses = [
'aback',
'abase',
'abate',
'abbey',
'abbot',
'abhor',
'abide',
'abled',
'abode',
'abort',
];
/// This class holds game state for a single round of Bulls and Cows,
/// and exposes methods that a UI would need to manage the game state.
///
/// On it's own, this class won't manage a game.
/// It assumes that a client will use its methods to progress through a game.
class Game {
Game({this.numAllowedGuesses = defaultNumGuesses, this.seed}) {
_wordToGuess = seed == null ? Word.random() : Word.fromSeed(seed!);
_guesses = List<Word>.filled(numAllowedGuesses, Word.empty());
}
late final int numAllowedGuesses;
late List<Word> _guesses;
late Word _wordToGuess;
int? seed;
Word get hiddenWord => _wordToGuess;
UnmodifiableListView<Word> get guesses => UnmodifiableListView(_guesses);
Word get previousGuess {
final index = _guesses.lastIndexWhere((word) => word.isNotEmpty);
return index == -1 ? Word.empty() : _guesses[index];
}
int get activeIndex {
return _guesses.indexWhere((word) => word.isEmpty);
}
int get guessesRemaining {
if (activeIndex == -1) return 0;
return numAllowedGuesses - activeIndex;
}
void resetGame() {
_wordToGuess = seed == null ? Word.random() : Word.fromSeed(seed!);
_guesses = List.filled(numAllowedGuesses, Word.empty());
}
// Most common entry-point for handling guess logic.
// For finer control over logic, use other methods such as [isGuessLegal]
// and [matchGuess]
Word guess(String guess) {
final result = matchGuessOnly(guess);
addGuessToList(result);
return result;
}
bool get didWin {
if (_guesses.first.isEmpty) return false;
for (final letter in previousGuess) {
if (letter.type != HitType.hit) return false;
}
return true;
}
bool get didLose => guessesRemaining == 0 && !didWin;
// UIs can call this method before calling [guess] if they want
// to show users messages based incorrect words
bool isLegalGuess(String guess) {
return Word.fromString(guess).isLegalGuess;
}
// Doesn't move the game forward, only executes match logic.
Word matchGuessOnly(String guess) {
// The hidden word will be used by subsequent guesses.
final hiddenCopy = Word.fromString(_wordToGuess.toString());
return Word.fromString(guess).evaluateGuess(hiddenCopy);
}
void addGuessToList(Word guess) {
final i = _guesses.indexWhere((word) => word.isEmpty);
_guesses[i] = guess;
}
}
class Word with IterableMixin<Letter> {
Word(this._letters);
factory Word.empty() {
return Word(List.filled(5, (char: '', type: HitType.none)));
}
factory Word.fromString(String guess) {
final list = guess.toLowerCase().split('');
final letters = list
.map((String char) => (char: char, type: HitType.none))
.toList();
return Word(letters);
}
factory Word.random() {
final rand = Random();
final nextWord = legalWords[rand.nextInt(legalWords.length)];
return Word.fromString(nextWord);
}
factory Word.fromSeed(int seed) {
return Word.fromString(legalWords[seed % legalWords.length]);
}
final List<Letter> _letters;
/// Loop over the Letters in this word
@override
Iterator<Letter> get iterator => _letters.iterator;
@override
bool get isEmpty {
return every((letter) => letter.char.isEmpty);
}
@override
bool get isNotEmpty => !isEmpty;
Letter operator [](int i) => _letters[i];
void operator []=(int i, Letter value) => _letters[i] = value;
@override
String toString() {
return _letters.map((Letter c) => c.char).join().trim();
}
// Used to play game in the CLI implementation
String toStringVerbose() {
return _letters.map((l) => '${l.char} - ${l.type.name}').join('\n');
}
}
// Domain specific methods that contain word related logic.
extension WordUtils on Word {
bool get isLegalGuess {
if (!allLegalGuesses.contains(toString())) {
return false;
}
return true;
}
/// Compares two [Word] objects and returns a new [Word] that
/// has the same letters as this word, but each [Letter]
/// has new a [HitType] of either [HitType.hit],
/// [HitType.partial], or [HitType.miss].
Word evaluateGuess(Word other) {
assert(isLegalGuess);
// Find exact hits. Mark them as hits, and mark letters in the hidden word
// as removed.
for (var i = 0; i < length; i++) {
if (other[i].char == this[i].char) {
this[i] = (char: this[i].char, type: HitType.hit);
other[i] = (char: other[i].char, type: HitType.removed);
}
}
// Find the partial matches
// The outer loop is through the hidden word
for (var i = 0; i < other.length; i++) {
// If a letter in the hidden word is already marked as "removed",
// then it's already an exact match, so skip it
final targetLetter = other[i];
if (targetLetter.type != HitType.none) continue;
// Loop through the guessed word once for each letter in the hidden word
for (var j = 0; j < length; j++) {
final guessedLetter = this[j];
// skip letters that have already been marked as exact matches
if (guessedLetter.type != HitType.none) continue;
// If this letter, which must not be in the same position, is the same,
// it's a partial match
if (guessedLetter.char == targetLetter.char) {
this[j] = (char: guessedLetter.char, type: HitType.partial);
other[i] = (char: targetLetter.char, type: HitType.removed);
break;
}
}
}
// Mark remaining letters in guessed word as misses
for (var i = 0; i < length; i++) {
if (this[i].type == HitType.none) {
this[i] = (char: this[i].char, type: HitType.miss);
}
}
return this;
}
}