commit c843003b66725030e05ee975e5c47679f3d433d6 Author: Glenn Date: Sun Aug 21 13:43:39 2022 +0200 Initial import diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..77248d0 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ + +all: build + +build: + shards build + +clean: + shards clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..2fc1018 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Bordle + +Play Wordle in your favorite terminal. Implemented in Crystal. + + diff --git a/bin/bordle b/bin/bordle new file mode 100755 index 0000000..3aed7c5 Binary files /dev/null and b/bin/bordle differ diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..6dea941 --- /dev/null +++ b/shard.yml @@ -0,0 +1,24 @@ +name: draft-bordle +version: 0.1.0 + +authors: + - Glenn Y. Rolland + +description: | + A stupid wordle-like game for your terminal + +# dependencies: +# pg: +# github: will/crystal-pg +# version: "~> 0.5" + +# development_dependencies: +# webmock: +# github: manastech/webmock.cr + +license: LGPL-3 + +targets: + bordle: + main: src/main.cr + diff --git a/src/dictionary.cr b/src/dictionary.cr new file mode 100644 index 0000000..75ffb26 --- /dev/null +++ b/src/dictionary.cr @@ -0,0 +1,40 @@ + +class Bordle + class Dictionary + DICTIONARY_FILE = "/usr/share/dict/french" + + property length : UInt8 + property data : Array(String) + + def initialize(length : UInt8) + @length = length + @data = [] of String + load_data + + end + + def load_data + # use french dictionary from wfrench package + if ! File.exists? DICTIONARY_FILE + STDERR.puts "ERROR: dictionary file missing! (#{DICTIONARY_FILE})" + STDERR.puts " Please install then wfrench package on your system" + exit 1 + end + + lines = File.read_lines(DICTIONARY_FILE) + @data = + lines + .select {|word| word.size == @length } + .map { |word| word.tr( TR_DIACRITICS, TR_ASCII ) } + end + def includes?(word) + @data.includes? word + end + + def choose_word + @data + .sample(1) + .first + end + end +end diff --git a/src/game.cr b/src/game.cr new file mode 100644 index 0000000..fc6f56c --- /dev/null +++ b/src/game.cr @@ -0,0 +1,68 @@ + +require "colorize" + +require "./types" +require "./dictionary" +require "./target_word" + +class Bordle + class Game + def initialize + @length = 5_u8 + @dict = Dictionary.new(@length) + @target = TargetWord.new(@dict.choose_word) + end + + def display(diff : Array(LetterScore), try) + printf("#{try}. ") + diff.each do |ls| + colors = case ls[1] + when Score::NotOk then {:white, :black} + when Score::WrongPlace then {:white, :yellow} + when Score::RightPlace then {:white, :green} + else {:white, :black} + end + str = ("%s" % ls[0]).colorize.fore(colors[0]).back(colors[1]) + printf("%s", str) + end + puts "" + end + + def input_word(try) + word = "" + loop do + printf "#{try}. " + word = gets() + + word = "" if word.nil? + word.tr(TR_DIACRITICS, TR_ASCII).downcase + + break if word.size == @length && @dict.includes? word + printf("\x1B[A\x1B[2K") + end + word + end + + def run + printf " .....\n" + try = 0 + while true + try += 1 + word = input_word(try) + printf("\x1B[A\x1B[2K") + + diff = @target.diff(word) + display(diff, try) + if @target.equals?(word) + puts "-- GAGNÉ ! --".colorize.fore(:green) + return + end + if try >= 5 + puts "-- PERDU ! --".colorize.fore(:red) + return + end + end + end + end +end + diff --git a/src/main.cr b/src/main.cr new file mode 100644 index 0000000..9406659 --- /dev/null +++ b/src/main.cr @@ -0,0 +1,36 @@ + +require "option_parser" + +require "./game" + +class Bordle + class BordleCli + + property options : String? + + def initialize() + @options = nil + end + + def self.parse_options(args) + return nil + end + + # FIXME: add --length LEN option (length of words) + # FIXME: add --lang LANG option (choose dictionnary) + # FIXME: add --tries TRIES option (how many tries are allowed) + # FIXME: add --with-letters (show used/unused letters) + def self.run(args) + app = BordleCli.new + options = BordleCli.parse_options(args) + app.options = options + + game = Game.new + game.run() + + end + end +end + +Bordle::BordleCli.run(ARGV) + diff --git a/src/target_word.cr b/src/target_word.cr new file mode 100644 index 0000000..0c121aa --- /dev/null +++ b/src/target_word.cr @@ -0,0 +1,61 @@ + +class Bordle + class TargetWord + def initialize(@target_word : String) + end + + def equals?(word) + @target_word == word + end + + def to_h() + hash = Hash(Char, Array(Int32)).new + @target_word.each_char_with_index do |char, index| + hash[char] = [] of Int32 unless hash.has_key? char + hash[char] << index + end + hash + end + + def diff(word) + hash = self.to_h + result = [] of LetterScore + + # REF = r a t e s + # TEST= c e r e t + + word.each_char_with_index do |char, index| + if ! hash.has_key? char + result << {char, Score::NotOk} + next + end + + char_values = hash[char] + char_misplaced = char_values.select {|pos| @target_word[pos] != word[pos] } + char_wellplaced = char_values.select {|pos| @target_word[pos] == word[pos] } + # puts "values(#{char}) = #{char_values}" + # puts "misplaced(#{char}) = #{char_misplaced}" + # puts "wellplaced(#{char}) = #{char_wellplaced}" + + if char_wellplaced.includes? index + result << {char, Score::RightPlace} + char_wellplaced.reject! {|pos| pos == index } + hash[char] = char_misplaced + char_wellplaced + hash.delete(char) if hash[char].empty? + next + end + + if ! char_misplaced.empty? + result << {char, Score::WrongPlace} + char_misplaced = char_misplaced.skip(1) + hash[char] = char_misplaced + char_wellplaced + hash.delete(char) if hash[char].empty? + next + end + + result << {char, Score::NotOk} + end + result + end + end +end diff --git a/src/types.cr b/src/types.cr new file mode 100644 index 0000000..3795c47 --- /dev/null +++ b/src/types.cr @@ -0,0 +1,14 @@ + +class Bordle + TR_DIACRITICS = "ÀÁÂÃÄÅàáâãäåĀāĂ㥹ÇçĆćĈĉĊċČčÐðĎďĐđÈÉÊËèéêëĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħÌÍÎÏìíîïĨĩĪīĬĭĮįİıĴĵĶķĸĹĺĻļĽľĿŀŁłÑñŃńŅņŇňʼnŊŋÒÓÔÕÖØòóôõöøŌōŎŏŐőŔŕŖŗŘřŚśŜŝŞşŠšȘșſŢţŤťŦŧȚțÙÚÛÜùúûüŨũŪūŬŭŮůŰűŲųŴŵÝýÿŶŷŸŹźŻżŽž" + TR_ASCII = "AAAAAAaaaaaaAaAaAaCcCcCcCcCcDdDdDdEEEEeeeeEeEeEeEeEeGgGgGgGgHhHhIIIIiiiiIiIiIiIiIiJjKkkLlLlLlLlLlNnNnNnNnnNnOOOOOOooooooOoOoOoRrRrRrSsSsSsSsSssTtTtTtTtUUUUuuuuUuUuUuUuUuUuWwYyyYyYZzZzZz" + + + enum Score + RightPlace = 0 + WrongPlace = 1 + NotOk = 2 + end + + alias LetterScore = {Char, Score} +end