diff --git a/kadai3-1/lfcd85/.gitignore b/kadai3-1/lfcd85/.gitignore new file mode 100644 index 0000000..3b01975 --- /dev/null +++ b/kadai3-1/lfcd85/.gitignore @@ -0,0 +1 @@ +bin/typinggame diff --git a/kadai3-1/lfcd85/Makefile b/kadai3-1/lfcd85/Makefile new file mode 100644 index 0000000..996b188 --- /dev/null +++ b/kadai3-1/lfcd85/Makefile @@ -0,0 +1,12 @@ +build: cmd/*.go typinggame/*.go words/*.go + GO111MODULE=on go build -o bin/typinggame cmd/main.go + +fmt: + go fmt ./... + go vet ./... + +check: + GO111MODULE=on go test ./... -v + +coverage: + GO111MODULE=on go test ./... -cover diff --git a/kadai3-1/lfcd85/README.md b/kadai3-1/lfcd85/README.md new file mode 100644 index 0000000..eb1beba --- /dev/null +++ b/kadai3-1/lfcd85/README.md @@ -0,0 +1,41 @@ +# Typing Game + +An implementation for the typing game, kadai3-1 of Gopherdojo #5. The standard library packages for Golang are the words for typing. + +Gopher道場 #5 課題3-1 `タイピングゲームを作ろう` の実装です。Go言語の標準ライブラリのパッケージ名がタイピング対象の単語になっています。 + +## Installation + +```bash +$ make build +``` + +## Usage + +The game starts after executing the command below. Let's type the shown package name. Your score will be shown after reaching to the time limit and finishing the game. + +下記のコマンドを実行するとゲームが開始します。表示されたパッケージ名をタイプしてください。制限時間に達するとゲームが終了し、スコアが表示されます。 + +```bash +$ bin/typinggame +Let's type the standard package names! ( Time limit: 30s ) +> hash/fnv +hash/fnv +hash/fnv ... OK! current score: 1 + +> debug +d +d ... NG: try again. +> debug + +30s has passed: you correctly typed 1 package(s)! +``` + +The time limit can be set by `-t` option. Default value is 30 sec. + +制限時間は `-t` オプションで変更可能です。デフォルトは30秒です。 + +```bash +$ bin/typinggame -t 10 +Let's type the standard package names! ( Time limit: 10s ) +``` diff --git a/kadai3-1/lfcd85/bin/.gitkeep b/kadai3-1/lfcd85/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/kadai3-1/lfcd85/cmd/main.go b/kadai3-1/lfcd85/cmd/main.go new file mode 100644 index 0000000..4fca9ca --- /dev/null +++ b/kadai3-1/lfcd85/cmd/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "flag" + "fmt" + "time" + + "github.com/gopherdojo/dojo5/kadai3-1/lfcd85/typinggame" + "github.com/gopherdojo/dojo5/kadai3-1/lfcd85/words" +) + +const path = "./testdata/go_standard_library.txt" + +func main() { + timeLimit := flag.Int("t", 30, "Time limit of the game (sec)") + flag.Parse() + + words, err := words.Import(path) + if err != nil { + fmt.Println("error:", err) + return + } + g := typinggame.Game{ + Words: words, + TimeLimit: time.Duration(*timeLimit) * time.Second, + } + + if err := typinggame.Execute(g); err != nil { + fmt.Println("error:", err) + return + } +} diff --git a/kadai3-1/lfcd85/go.mod b/kadai3-1/lfcd85/go.mod new file mode 100644 index 0000000..d2f0782 --- /dev/null +++ b/kadai3-1/lfcd85/go.mod @@ -0,0 +1,5 @@ +module github.com/gopherdojo/dojo5/kadai3-1/lfcd85 + +go 1.12 + +require github.com/hashicorp/go-multierror v1.0.0 diff --git a/kadai3-1/lfcd85/go.sum b/kadai3-1/lfcd85/go.sum new file mode 100644 index 0000000..d2f1330 --- /dev/null +++ b/kadai3-1/lfcd85/go.sum @@ -0,0 +1,4 @@ +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= diff --git a/kadai3-1/lfcd85/testdata/abc.txt b/kadai3-1/lfcd85/testdata/abc.txt new file mode 100644 index 0000000..de98044 --- /dev/null +++ b/kadai3-1/lfcd85/testdata/abc.txt @@ -0,0 +1,3 @@ +a +b +c diff --git a/kadai3-1/lfcd85/testdata/go_standard_library.txt b/kadai3-1/lfcd85/testdata/go_standard_library.txt new file mode 100644 index 0000000..46f3462 --- /dev/null +++ b/kadai3-1/lfcd85/testdata/go_standard_library.txt @@ -0,0 +1,154 @@ +archive +archive/tar +archive/zip +bufio +builtin +bytes +compress +compress/bzip2 +compress/flate +compress/gzip +compress/lzw +compress/zlib +container +container/heap +container/list +container/ring +context +crypto +crypto/aes +crypto/cipher +crypto/des +crypto/dsa +crypto/ecdsa +crypto/elliptic +crypto/hmac +crypto/md5 +crypto/rand +crypto/rc4 +crypto/rsa +crypto/sha1 +crypto/sha256 +crypto/sha512 +crypto/subtle +crypto/tls +crypto/x509 +crypto/x509/pkix +database +database/sql +database/sql/driver +debug +debug/dwarf +debug/elf +debug/gosym +debug/macho +debug/pe +debug/plan9obj +encoding +encoding/ascii85 +encoding/asn1 +encoding/base32 +encoding/base64 +encoding/binary +encoding/csv +encoding/gob +encoding/hex +encoding/json +encoding/pem +encoding/xml +errors +expvar +flag +fmt +go +go/ast +go/build +go/constant +go/doc +go/format +go/importer +go/parser +go/printer +go/scanner +go/token +go/types +hash +hash/adler32 +hash/crc32 +hash/crc64 +hash/fnv +html +html/template +image +image/color +image/color/palette +image/draw +image/gif +image/jpeg +image/png +index +index/suffixarray +io +io/ioutil +log +log/syslog +math +math/big +math/bits +math/cmplx +math/rand +mime +mime/multipart +mime/quotedprintable +net +net/http +net/http/cgi +net/http/cookiejar +net/http/fcgi +net/http/httptest +net/http/httptrace +net/http/httputil +net/http/pprof +net/mail +net/rpc +net/rpc/jsonrpc +net/smtp +net/textproto +net/url +os +os/exec +os/signal +os/user +path +path/filepath +plugin +reflect +regexp +regexp/syntax +runtime +runtime/cgo +runtime/debug +runtime/msan +runtime/pprof +runtime/race +runtime/trace +sort +strconv +strings +sync +sync/atomic +syscall +syscall/js +testing +testing/iotest +testing/quick +text +text/scanner +text/tabwriter +text/template +text/template/parse +time +unicode +unicode/utf16 +unicode/utf8 +unsafe diff --git a/kadai3-1/lfcd85/typinggame/export_test.go b/kadai3-1/lfcd85/typinggame/export_test.go new file mode 100644 index 0000000..52c4547 --- /dev/null +++ b/kadai3-1/lfcd85/typinggame/export_test.go @@ -0,0 +1,3 @@ +package typinggame + +var ExportGameRun = (*Game).run diff --git a/kadai3-1/lfcd85/typinggame/typinggame.go b/kadai3-1/lfcd85/typinggame/typinggame.go new file mode 100644 index 0000000..68eb0ec --- /dev/null +++ b/kadai3-1/lfcd85/typinggame/typinggame.go @@ -0,0 +1,81 @@ +package typinggame + +import ( + "bufio" + "context" + "fmt" + "io" + "math/rand" + "os" + "time" + + "github.com/hashicorp/go-multierror" +) + +// Words stores a slice of words which is used for the game. +type Words []string + +// Game struct holds the words and the time limits of the game. +type Game struct { + Words Words + TimeLimit time.Duration +} + +// Execute starts the game using standard input and output. +func Execute(g Game) error { + err := g.run(inputChannel(os.Stdin), os.Stdout) + return err +} + +func (g *Game) run(ch <-chan string, w io.Writer) error { + var result error + + bc := context.Background() + ctx, cancel := context.WithTimeout(bc, g.TimeLimit) + defer cancel() + + result = printWithMultiErr(w, result, "Let's type the standard package names! ( Time limit:", g.TimeLimit, ")") + + var score int + rand.Seed(time.Now().UnixNano()) + word := g.Words[rand.Intn(len(g.Words))] +LOOP: + for { + result = printWithMultiErr(w, result, ">", word) + select { + case input := <-ch: + if input == word { + score++ + result = printWithMultiErr(w, result, input, "... OK! current score:", score) + word = g.Words[rand.Intn(len(g.Words))] + } else { + result = printWithMultiErr(w, result, input, "... NG: try again.") + } + case <-ctx.Done(): + result = printWithMultiErr(w, result) + result = printWithMultiErr(w, result, g.TimeLimit, "has passed: you correctly typed", score, "package(s)!") + break LOOP + } + } + + return result +} + +func inputChannel(r io.Reader) <-chan string { + ch := make(chan string) + + go func() { + s := bufio.NewScanner(r) + for s.Scan() { + ch <- s.Text() + } + }() + return ch +} + +func printWithMultiErr(w io.Writer, result error, a ...interface{}) error { + if _, err := fmt.Fprintln(w, a...); err != nil { + result = multierror.Append(result, err) + } + return result +} diff --git a/kadai3-1/lfcd85/typinggame/typinggame_test.go b/kadai3-1/lfcd85/typinggame/typinggame_test.go new file mode 100644 index 0000000..9a03ccf --- /dev/null +++ b/kadai3-1/lfcd85/typinggame/typinggame_test.go @@ -0,0 +1,68 @@ +package typinggame_test + +import ( + "bytes" + "regexp" + "testing" + "time" + + "github.com/gopherdojo/dojo5/kadai3-1/lfcd85/typinggame" +) + +func initGame() typinggame.Game { + return typinggame.Game{ + typinggame.Words{"hoge"}, + 1 * time.Second, + } +} + +func TestExecute(t *testing.T) { + g := initGame() + + if err := typinggame.Execute(g); err != nil { + t.Errorf("failed to execute new game: %v", err) + } +} + +func TestGame_run(t *testing.T) { + g := initGame() + + ch := make(chan string) + go func() { + time.Sleep(100 * time.Millisecond) + ch <- "hoga" + time.Sleep(100 * time.Millisecond) + ch <- "hoge" + }() + + var output bytes.Buffer + typinggame.ExportGameRun(&g, ch, &output) + + cases := []struct { + output string + expected bool + }{ + {"hoga ... NG", true}, + {"hoga ... OK", false}, + {"hoge ... OK", true}, + {"hoge ... NG", false}, + {"you correctly typed 1 package", true}, + } + + for _, c := range cases { + c := c + t.Run(c.output, func(t *testing.T) { + t.Parallel() + + actual := regexp.MustCompile(c.output).MatchString(output.String()) + if actual != c.expected { + switch c.expected { + case true: + t.Errorf("%v should be outputted but actually was not", c.output) + case false: + t.Errorf("%v should not be outputted but actyally was", c.output) + } + } + }) + } +} diff --git a/kadai3-1/lfcd85/words/words.go b/kadai3-1/lfcd85/words/words.go new file mode 100644 index 0000000..9809bb6 --- /dev/null +++ b/kadai3-1/lfcd85/words/words.go @@ -0,0 +1,35 @@ +package words + +import ( + "bufio" + "os" + + "github.com/gopherdojo/dojo5/kadai3-1/lfcd85/typinggame" + "github.com/hashicorp/go-multierror" +) + +// Import reads the text file and returns the words for the typing game. +func Import(path string) (typinggame.Words, error) { + var words typinggame.Words + var result error + + f, err := os.Open(path) + if err != nil { + result = multierror.Append(result, err) + } + defer func() { + if err := f.Close(); err != nil { + result = multierror.Append(result, err) + } + }() + + s := bufio.NewScanner(f) + for s.Scan() { + words = append(words, s.Text()) + } + if err := s.Err(); err != nil { + result = multierror.Append(result, err) + } + + return words, result +} diff --git a/kadai3-1/lfcd85/words/words_test.go b/kadai3-1/lfcd85/words/words_test.go new file mode 100644 index 0000000..52f4fc8 --- /dev/null +++ b/kadai3-1/lfcd85/words/words_test.go @@ -0,0 +1,40 @@ +package words_test + +import ( + "testing" + + "github.com/gopherdojo/dojo5/kadai3-1/lfcd85/words" +) + +func TestImport(t *testing.T) { + cases := []struct { + path string + firstWord string + lastWord string + length int + }{ + {"../testdata/abc.txt", "a", "c", 3}, + {"../testdata/go_standard_library.txt", "archive", "unsafe", 154}, + } + + for _, c := range cases { + c := c + t.Run(c.path, func(t *testing.T) { + t.Parallel() + + words, err := words.Import(c.path) + if err != nil { + t.Errorf("failed to Import %v: %v", c.path, err) + } + if words[0] != c.firstWord { + t.Errorf("the first item of imported words is %v; it should be %v", words[0], c.firstWord) + } + if words[len(words)-1] != c.lastWord { + t.Errorf("the first item of imported words is %v; it should be %v", words[0], c.lastWord) + } + if len(words) != c.length { + t.Errorf("the length of imported words is %v; it should be %v", len(words), c.length) + } + }) + } +}