Skip to content

Commit

Permalink
Adds 100% coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
Luckey-Elijah committed Mar 29, 2022
1 parent 92ab65d commit b27076e
Show file tree
Hide file tree
Showing 32 changed files with 477 additions and 142 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,6 @@ Start playing:
clordle play
```

![Gameplay of world - A Wordle clone in the command line.](assets/gameplay.png)
# Demo

![Gameplay of world - A Wordle clone in the command line.](assets/demo.gif)
6 changes: 1 addition & 5 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
include: package:very_good_analysis/analysis_options.yaml

linter:
rules:
public_member_api_docs: false
include: package:very_good_analysis/analysis_options.yaml
Binary file added assets/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed assets/gameplay.png
Binary file not shown.
4 changes: 4 additions & 0 deletions lib/src/commands/clordle_command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import 'package:args/command_runner.dart';
import 'package:clordle/clordle.dart';
import 'package:mason_logger/mason_logger.dart';

/// {@template clordle_command_runner}
/// Entry point to the `clordle` cli game.
/// {@endtemplate}
class ClordleCommandRunner extends CommandRunner<ExitCode> {
/// {@macro clordle_command_runner}
ClordleCommandRunner({
Logger? logger,
}) : _logger = logger ?? Logger(),
Expand Down
52 changes: 16 additions & 36 deletions lib/src/commands/play.dart
Original file line number Diff line number Diff line change
@@ -1,62 +1,42 @@
import 'package:args/args.dart';
import 'dart:math';

import 'package:args/command_runner.dart';
import 'package:clordle/clordle.dart';
import 'package:mason_logger/mason_logger.dart';

/// {@template play_command}
/// Entry point to the `play` sub-command.
/// {@endtemplate}
class PlayCommand extends Command<ExitCode> {
PlayCommand(this.logger) {
argParser
..addOption(
'word',
abbr: 'w',
help: 'The target word. Used for debugging.',
)
..addOption(
'max',
abbr: 'm',
defaultsTo: '6',
help: 'The max number of guesses/tries.',
);
}
/// {@macro play_command}
PlayCommand(this._logger, [List<String> seed = words]) : _words = seed;

final Logger logger;
static const _defaultGuesses = 6;
ArgResults get _argResults => argResults!;
final Logger _logger;
final List<String> _words;

@override
ExitCode run() {
final max = int.tryParse(_argResults['max'] as String) ?? _defaultGuesses;
final word = _argResults['word'] as String? ?? getWord();
final game = GameState(wordle: word, maxGuesses: max);
final word = getWord(_words, Random().nextInt);
final game = GameState(wordle: word);

while (_gameIsContinued(game)) {
logger
while (gameShouldContinue(game, _logger.prompt)) {
_logger
..write(gameboard(game.guesses).join('\n'))
..write('\n')
..info(
'${game.remainingTurns} remain guess'
'${game.remainingTurns == 1 ? '' : 'es'}.',
)
..info(remainingGuessesLabel(game.remainingTurns))
..write(keyboard(playedLetters: game.letterGuesses).join('\n'))
..write('\n');
}

if (game.status == GameStatus.win) {
logger.success('You won!');
_logger.success('You won!');
} else {
logger.err('You lost! The word was ${game.wordle}');
_logger.err('You lost! The word was ${game.wordle}');
}

return ExitCode.success;
}

bool _gameIsContinued(GameState game) {
return game.guess(
logger.prompt('GUESS:').toUpperCase().padRight(5).substring(0, 5),
) ==
GameStatus.cont;
}

@override
String get description => 'Start the Clordle game.';

Expand Down
65 changes: 18 additions & 47 deletions lib/src/models/game_state.dart
Original file line number Diff line number Diff line change
@@ -1,68 +1,39 @@
import 'package:equatable/equatable.dart';

enum LetterState { hit, miss, close, unmatched }

enum GameStatus { win, loss, cont }

class Letter extends Equatable {
const Letter(this.state, this.character);

final LetterState state;
final String character;

@override
List<Object> get props => [state, character];
}

class Word extends Equatable {
const Word(this.letters);

factory Word.fromGuess(String guess, String wordle) =>
Word(letterMapper(guess, wordle).toList());

final List<Letter> letters;

bool get isMatch => letters.every((l) => l.state == LetterState.hit);

@override
List<Object> get props => letters;

static Iterable<Letter> letterMapper(String guess, String wordle) sync* {
final guessLetters = guess.split('');
final wordleLetters = wordle.split('');

for (var i = 0; i < guessLetters.length; i++) {
final gLetter = guessLetters[i];
if (gLetter == wordleLetters[i]) {
yield Letter(LetterState.hit, gLetter);
} else if (wordle.contains(gLetter)) {
yield Letter(LetterState.close, gLetter);
} else {
yield Letter(LetterState.miss, gLetter);
}
}
}
}
import 'package:clordle/clordle.dart';

/// {@template game_state}
/// The state of a given game.
/// {@endtemplate}
class GameState {
/// {@macro game_state}
GameState({
required String wordle,
this.maxGuesses = 6,
}) : wordle = wordle.toUpperCase();

/// All the previously guessed words.
List<Word> get guesses => _guesses;

/// The number of turns remaining in this game.
int get remainingTurns => maxGuesses - guesses.length;

/// A set of all the letters that have been play. Useful for creating a
/// keyboard state.
Set<Letter> get letterGuesses =>
_guesses.expand<Letter>((w) => w.letters).toSet();

final List<Word> _guesses = [];

/// The goal word that is too be guessed.
final String wordle;

/// The maximum number of guesses allowed in this game.
final int maxGuesses;

/// This game's current status.
late GameStatus status;

/// When a guess is made, the word is evaluated and returns the appropiate
/// [GameStatus].
/// When a guess is made, the word is evaluated and returns the appropriate
/// [GameStatus]. [status] is also updated when this method is invoked.
GameStatus guess(String guess) {
final word = Word.fromGuess(guess.toUpperCase(), wordle);

Expand Down
16 changes: 16 additions & 0 deletions lib/src/models/game_status.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/// Represents the state/status of a given game.
enum GameStatus {
/// {@template game_status}
/// Game is in a
/// {@endtemplate}
/// win state.
win,

/// {@macro game_status}
/// loss state.
loss,

/// {@macro game_status}
/// can continue state.
cont,
}
19 changes: 19 additions & 0 deletions lib/src/models/letter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'package:clordle/clordle.dart';
import 'package:equatable/equatable.dart';

/// {@template letter}
/// The game's representation of a letter.
/// {@endtemplate}
class Letter extends Equatable {
/// {@macro letter}
const Letter(this.status, this.character);

/// The current state of this letter.
final LetterStatus status;

/// This letter's character string.
final String character;

@override
List<Object> get props => [status, character];
}
20 changes: 20 additions & 0 deletions lib/src/models/letter_status.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// The state or status of any given letter in the game.
enum LetterStatus {
/// {@template letter_status}
/// Represents a letter that
/// {@endtemplate}
/// is matching.
hit,

/// {@macro letter_status}
/// does not match.
miss,

/// {@macro letter_status}
/// is in the same word, but the incorrect location.
close,

/// {@macro letter_status}
/// has not yet been check/played.
unmatched,
}
4 changes: 4 additions & 0 deletions lib/src/models/models.dart
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export 'game_state.dart';
export 'game_status.dart';
export 'letter.dart';
export 'letter_status.dart';
export 'word.dart';
39 changes: 39 additions & 0 deletions lib/src/models/word.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'package:clordle/clordle.dart';
import 'package:equatable/equatable.dart';

/// {@template word}
/// The game's representation of a word.
/// {@endtemplate}
class Word extends Equatable {
/// {@macro word}
const Word(this.letters);

/// Generate a word from a guess.
factory Word.fromGuess(String guess, String wordle) =>
Word(_letterMapper(guess, wordle).toList());

/// The letters that represent this word.
final List<Letter> letters;

/// Whether each letter in this words is a matching letter.
bool get isMatch => letters.every((l) => l.status == LetterStatus.hit);

static Iterable<Letter> _letterMapper(String guess, String wordle) sync* {
final guessLetters = guess.split('');
final wordleLetters = wordle.split('');

for (var i = 0; i < guessLetters.length; i++) {
final gLetter = guessLetters[i];
if (gLetter == wordleLetters[i]) {
yield Letter(LetterStatus.hit, gLetter);
} else if (wordle.contains(gLetter)) {
yield Letter(LetterStatus.close, gLetter);
} else {
yield Letter(LetterStatus.miss, gLetter);
}
}
}

@override
List<Object> get props => [letters];
}
9 changes: 9 additions & 0 deletions lib/src/utils/game_should_continue.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'package:clordle/clordle.dart';

/// Evaluates the given [game] and determines if the game has not ended.
bool gameShouldContinue(GameState game, String Function(String) guesser) {
return game.guess(
guesser('GUESS:').toUpperCase().padRight(5).substring(0, 5),
) ==
GameStatus.cont;
}
12 changes: 8 additions & 4 deletions lib/src/utils/gameboard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Iterable<String> gameboard(Iterable<Word> words) sync* {
throw Exception('The word must be 5 chars long.');
}

yield gameboardRow(word).join();
yield gameboardRow(word, close, hit).join();
}

yield '└───┴───┴───┴───┴───┘';
Expand All @@ -40,13 +40,17 @@ Iterable<String> gameboard(Iterable<Word> words) sync* {
/// assert(row == '│ W │ O │ R │ D │ S │');
/// ```
/// The word can be any length. A empty string will create a single `'│'`.
Iterable<String> gameboardRow(Word word) sync* {
Iterable<String> gameboardRow(
Word word,
String Function(String) close,
String Function(String) hit,
) sync* {
yield '│';

for (final letter in word.letters) {
if (letter.state == LetterState.hit) {
if (letter.status == LetterStatus.hit) {
yield ' ${hit(letter.character)} │';
} else if (letter.state == LetterState.close) {
} else if (letter.status == LetterStatus.close) {
yield ' ${close(letter.character)} │';
} else {
yield ' ${letter.character} │';
Expand Down
11 changes: 3 additions & 8 deletions lib/src/utils/get_word.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
import 'dart:math';

import 'package:clordle/clordle.dart';

String getWord() {
final index = Random().nextInt(words.length);
return words[index];
}
/// Select the word from [words] using the selector.
String getWord(List<String> words, int Function(int) selector) =>
words[selector(words.length)];
Loading

0 comments on commit b27076e

Please sign in to comment.