diff --git a/kadai3/execjosh/go-typing-game/Makefile b/kadai3/execjosh/go-typing-game/Makefile new file mode 100644 index 0000000..0a71b6f --- /dev/null +++ b/kadai3/execjosh/go-typing-game/Makefile @@ -0,0 +1,14 @@ +WORDS_ALPHA='https://github.com/dwyl/english-words/raw/master/words_alpha.zip' + +all: setup + +.PHONY: setup +setup: + [ ! -f words_alpha.txt ] && wget -Owords_alpha.zip $(WORDS_ALPHA) && unzip words_alpha.zip && rm words_alpha.zip + +.PHONY: coverage +coverage: + @rm coverage.out || true + go get -u github.com/haya14busa/goverage + '${GOPATH}/bin/goverage' -v -coverprofile coverage.out ./internal/game ./internal/wordbank + go tool cover -html=coverage.out diff --git a/kadai3/execjosh/go-typing-game/internal/game/game.go b/kadai3/execjosh/go-typing-game/internal/game/game.go new file mode 100644 index 0000000..a196761 --- /dev/null +++ b/kadai3/execjosh/go-typing-game/internal/game/game.go @@ -0,0 +1,86 @@ +package game + +import ( + "bufio" + "fmt" + "io" + "time" + + "github.com/gopherdojo/dojo1/kadai3/execjosh/go-typing-game/internal/wordbank" +) + +type ctx struct { + done chan struct{} + sch, fch chan bool + + wb wordbank.WordProvider + + stdin io.Reader + stdout io.Writer +} + +// Run runs the game +func Run(stdin io.Reader, stdout io.Writer, wb wordbank.WordProvider, timeout time.Duration) *Stats { + ctx := &ctx{ + done: make(chan struct{}), + sch: make(chan bool), + fch: make(chan bool), + + wb: wb, + + stdin: stdin, + stdout: stdout, + } + + // Keep stats out of context to force use of channels + stats := &Stats{} + + go ctx.gameloop() + + go func() { + time.Sleep(timeout) + close(ctx.done) + }() + +OUT: + for { + select { + case _, ok := <-ctx.fch: + if ok { + stats.LogFailure() + } + case _, ok := <-ctx.sch: + if ok { + stats.LogSuccess() + } + case <-ctx.done: + close(ctx.sch) + close(ctx.fch) + break OUT + } + } + + return stats +} + +func (c *ctx) gameloop() { + in := bufio.NewScanner(c.stdin) + word := c.wb.NextWord() + + for { + fmt.Fprintln(c.stdout, word) + + if ok := in.Scan(); !ok { + break + } + + if word == in.Text() { + c.sch <- true + fmt.Fprintf(c.stdout, "\u2705\n") + word = c.wb.NextWord() + } else { + fmt.Fprintf(c.stdout, "\u274c\n") + c.fch <- true + } + } +} diff --git a/kadai3/execjosh/go-typing-game/internal/game/game_test.go b/kadai3/execjosh/go-typing-game/internal/game/game_test.go new file mode 100644 index 0000000..25e5244 --- /dev/null +++ b/kadai3/execjosh/go-typing-game/internal/game/game_test.go @@ -0,0 +1,39 @@ +package game_test + +import ( + "bytes" + "testing" + "time" + + "github.com/gopherdojo/dojo1/kadai3/execjosh/go-typing-game/internal/game" +) + +func TestRun(t *testing.T) { + input := bytes.NewBufferString("abc\nbcd\nc\n\n") + output := new(bytes.Buffer) + wb := wordBank{} + timeout := 1 * time.Second + + stats := game.Run(input, output, &wb, timeout) + + actualOutput := output.String() + expectedOutput := "abc\n✅\nabc\n❌\nabc\n❌\nabc\n❌\nabc\n" + if actualOutput != expectedOutput { + t.Fail() + } + + if stats.SuccessCount() != 1 { + t.Fail() + } + + if stats.FailureCount() != 3 { + t.Fail() + } +} + +type wordBank struct { +} + +func (wb *wordBank) NextWord() string { + return "abc" +} diff --git a/kadai3/execjosh/go-typing-game/internal/game/stats.go b/kadai3/execjosh/go-typing-game/internal/game/stats.go new file mode 100644 index 0000000..dcca856 --- /dev/null +++ b/kadai3/execjosh/go-typing-game/internal/game/stats.go @@ -0,0 +1,27 @@ +package game + +// Stats represents game stats +type Stats struct { + successCount int + failureCount int +} + +// SuccessCount returns the success count +func (s *Stats) SuccessCount() int { + return s.successCount +} + +// FailureCount returns the failure count +func (s *Stats) FailureCount() int { + return s.failureCount +} + +// LogSuccess logs a success +func (s *Stats) LogSuccess() { + s.successCount++ +} + +// LogFailure logs a failure +func (s *Stats) LogFailure() { + s.failureCount++ +} diff --git a/kadai3/execjosh/go-typing-game/internal/wordbank/wordbank.go b/kadai3/execjosh/go-typing-game/internal/wordbank/wordbank.go new file mode 100644 index 0000000..e236fc9 --- /dev/null +++ b/kadai3/execjosh/go-typing-game/internal/wordbank/wordbank.go @@ -0,0 +1,29 @@ +package wordbank + +import ( + "math/rand" +) + +// A WordProvider provides words +type WordProvider interface { + NextWord() string +} + +type randomWordBank struct { + words []string + rng *rand.Rand +} + +// NewRandomWordBank creates a new random word provider +func NewRandomWordBank(words []string, seed int64) WordProvider { + return &randomWordBank{ + words: words, + rng: rand.New(rand.NewSource(seed)), + } +} + +// NextWord implements WordProvider by returning a random word +func (rwp *randomWordBank) NextWord() string { + numWords := len(rwp.words) + return rwp.words[rwp.rng.Intn(numWords)] +} diff --git a/kadai3/execjosh/go-typing-game/internal/wordbank/wordbank_test.go b/kadai3/execjosh/go-typing-game/internal/wordbank/wordbank_test.go new file mode 100644 index 0000000..00d78c1 --- /dev/null +++ b/kadai3/execjosh/go-typing-game/internal/wordbank/wordbank_test.go @@ -0,0 +1,25 @@ +package wordbank_test + +import ( + "testing" + + "github.com/gopherdojo/dojo1/kadai3/execjosh/go-typing-game/internal/wordbank" +) + +func TestRandomWordProvider(t *testing.T) { + seed := int64(1234567890) + words := []string{ + "a", "b", "c", "d", "abc", + } + wb := wordbank.NewRandomWordBank(words, seed) + + expected := []string{ + "d", "b", "d", "b", "d", + } + + for _, exp := range expected { + if exp != wb.NextWord() { + t.Fail() + } + } +} diff --git a/kadai3/execjosh/go-typing-game/main.go b/kadai3/execjosh/go-typing-game/main.go new file mode 100644 index 0000000..114d48b --- /dev/null +++ b/kadai3/execjosh/go-typing-game/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + "os" + "time" + + "github.com/gopherdojo/dojo1/kadai3/execjosh/go-typing-game/internal/game" + "github.com/gopherdojo/dojo1/kadai3/execjosh/go-typing-game/internal/wordbank" +) + +const ( + wordsFilePath = "words_alpha.txt" + timeout = 30 * time.Second +) + +func die(err error) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) +} + +func main() { + wb, err := loadWordBankFromFile(wordsFilePath) + if err != nil { + die(err) + } + + fmt.Printf("You have %v!\n", timeout) + + stats := game.Run(os.Stdin, os.Stdout, wb, timeout) + + fmt.Println("\n---\nTime's Up, Grasshopper!") + fmt.Println("\x1b[92mSuccess count: ", stats.SuccessCount(), "\x1b[m") + fmt.Println("\x1b[31mFailure count: ", stats.FailureCount(), "\x1b[m") +} + +func loadWordBankFromFile(filepath string) (wordbank.WordProvider, error) { + f, err := os.Open(filepath) + if err != nil { + if os.IsNotExist(err) { + err = errors.New(fmt.Sprint("expected ", wordsFilePath, " to exist")) + } + return nil, err + } + defer f.Close() + + s := bufio.NewScanner(f) + var words []string + + for s.Scan() { + w := s.Text() + words = append(words, w) + } + + if err := s.Err(); err != nil { + return nil, err + } + + wb := wordbank.NewRandomWordBank(words, time.Now().UnixNano()) + + return wb, nil +}