diff --git a/kadai3-1/nagaa052/.gitignore b/kadai3-1/nagaa052/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/kadai3-1/nagaa052/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/kadai3-1/nagaa052/Makefile b/kadai3-1/nagaa052/Makefile new file mode 100644 index 0000000..4812eba --- /dev/null +++ b/kadai3-1/nagaa052/Makefile @@ -0,0 +1,29 @@ +NAME := tgame + +GO ?= go +BUILD_DIR=./build +BINARY ?= $(BUILD_DIR)/$(NAME) + +.PHONY: all +all: clean test build + +.PHONY: test +test: + $(GO) test -v -race ./... + +.PHONY: test_integration +test_integration: + $(GO) test -v -tags=integration ./... + +.PHONY: test_cover +test_cover: + $(GO) test -v -cover ./... + +.PHONY: clean +clean: + $(GO) clean + rm -f $(BINARY) + +.PHONY: build +build: + $(GO) build -o $(BINARY) -v diff --git a/kadai3-1/nagaa052/main.go b/kadai3-1/nagaa052/main.go new file mode 100644 index 0000000..afadfc4 --- /dev/null +++ b/kadai3-1/nagaa052/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + + "github.com/gopherdojo/dojo5/kadai3-1/nagaa052/pkg/game" +) + +func main() { + var timeout int + var color bool + flag.IntVar(&timeout, "t", game.DefaultOptions.TimeUpSecond, "Timeout Seconds") + flag.BoolVar(&color, "c", game.DefaultOptions.IsColor, "Print Color") + flag.Usage = usage + flag.Parse() + + g, err := game.New(game.Options{ + TimeUpSecond: timeout, + IsColor: color, + }, os.Stdin, os.Stdout, os.Stderr) + + if err != nil { + log.Fatal("Failed to start the game") + } + + os.Exit(g.Start()) +} + +func usage() { + fmt.Fprintf(os.Stderr, ` +tgame is a Typing Game +Usage: + tgame [option] +Options: +`) + flag.PrintDefaults() +} diff --git a/kadai3-1/nagaa052/pkg/game/color.go b/kadai3-1/nagaa052/pkg/game/color.go new file mode 100644 index 0000000..f6495bf --- /dev/null +++ b/kadai3-1/nagaa052/pkg/game/color.go @@ -0,0 +1,19 @@ +package game + +import ( + "fmt" + "io" +) + +type colorCode int + +const ( + red colorCode = 31 + green colorCode = 32 + yellow colorCode = 33 + blue colorCode = 34 +) + +func cFPrint(code colorCode, w io.Writer, text string) { + fmt.Fprintf(w, "\x1b[%dm%s\x1b[0m\n", code, text) +} diff --git a/kadai3-1/nagaa052/pkg/game/export_test.go b/kadai3-1/nagaa052/pkg/game/export_test.go new file mode 100644 index 0000000..ec074be --- /dev/null +++ b/kadai3-1/nagaa052/pkg/game/export_test.go @@ -0,0 +1,11 @@ +package game + +import ( + "github.com/gopherdojo/dojo5/kadai3-1/nagaa052/pkg/questions" +) + +var ExportGetQuestion = (*Game).getQuestion + +func (g *Game) ExportSetQs(qs *questions.Questions) { + g.qs = qs +} diff --git a/kadai3-1/nagaa052/pkg/game/game.go b/kadai3-1/nagaa052/pkg/game/game.go new file mode 100644 index 0000000..38707bd --- /dev/null +++ b/kadai3-1/nagaa052/pkg/game/game.go @@ -0,0 +1,174 @@ +/* +Typing game main logic +*/ +package game + +import ( + "bufio" + "context" + "fmt" + "io" + "math/rand" + "time" + + "github.com/gopherdojo/dojo5/kadai3-1/nagaa052/pkg/questions" +) + +const ( + ExitOK = iota + ExitError +) + +// Game is manages game information +type Game struct { + qs *questions.Questions + *Result + Options + inStream io.Reader + outStream, errStream io.Writer +} + +// Options is a specifiable option. +type Options struct { + TimeUpSecond int + IsColor bool +} + +// DefaultOptions is the default value of Options. +var DefaultOptions = Options{ + TimeUpSecond: 30, + IsColor: false, +} + +// Result is manages result information. +type Result struct { + Questions []*questions.Question + CorrectCount int +} + +// Print is Output the result. +func (r *Result) Print(out io.Writer) { + cr := float64(r.CorrectCount) / float64(len(r.Questions)) * 100 + fmt.Fprintln(out, "========================") + fmt.Fprintf(out, "Correct Count: %d\n", r.CorrectCount) + fmt.Fprintf(out, "Correct Rate: %.1f%\n", cr) +} + +// New is Generate a new game. +func New(opt Options, inStream io.Reader, outStream, errStream io.Writer) (*Game, error) { + if opt.TimeUpSecond <= 0 { + opt.TimeUpSecond = DefaultOptions.TimeUpSecond + } + + qs, err := questions.New() + if err != nil { + return nil, err + } + + return &Game{ + qs: qs, + Result: &Result{}, + Options: opt, + inStream: inStream, + outStream: outStream, + errStream: errStream, + }, nil +} + +// Start is Start Game +func (g *Game) Start() int { + g.printStart() + + bc := context.Background() + ctx, cannel := context.WithTimeout(bc, time.Duration(g.TimeUpSecond)*time.Second) + defer cannel() + + dst := g.startScanner() + + return func() int { + for { + q, err := g.getQuestion() + if err != nil { + fmt.Fprintf(g.errStream, "%v\n", err.Error()) + return ExitError + } + g.printQuestion(q.Word) + + select { + case <-ctx.Done(): + g.printTimeOut() + return ExitOK + case input := <-dst: + if q.IsCorrect(input) { + g.Result.CorrectCount++ + } + } + } + }() +} + +func (g *Game) startScanner() <-chan string { + + dst := make(chan string) + + go func() { + scanner := bufio.NewScanner(g.inStream) + defer close(dst) + + for scanner.Scan() { + dst <- scanner.Text() + } + }() + + return dst +} + +func (g *Game) getQuestion() (*questions.Question, error) { + + rand.Seed(time.Now().UnixNano()) + index := rand.Intn(g.qs.GetSize()) + + q, err := g.qs.GetOne(index) + if err != nil { + return nil, err + } + + g.Result.Questions = append(g.Result.Questions, q) + return q, nil +} + +func (g *Game) printStart() { + for i := 3; i > 0; i-- { + fmt.Fprintf(g.outStream, "%d", i) + time.Sleep(250 * time.Millisecond) + + fmt.Fprint(g.outStream, ".") + time.Sleep(250 * time.Millisecond) + fmt.Fprint(g.outStream, ".") + time.Sleep(250 * time.Millisecond) + fmt.Fprint(g.outStream, ".") + time.Sleep(250 * time.Millisecond) + } + fmt.Fprintln(g.outStream, "Start!!") + fmt.Fprintln(g.outStream, "========================") + time.Sleep(500 * time.Millisecond) +} + +func (g *Game) printQuestion(word string) { + if g.IsColor { + cFPrint(green, g.outStream, fmt.Sprintf("%s", word)) + } else { + fmt.Fprintf(g.outStream, "%s", word) + } +} + +func (g *Game) printTimeOut() { + if g.IsColor { + cFPrint(red, g.outStream, "\nTimeUp!!!") + } else { + fmt.Fprintf(g.outStream, "\nTimeUp!!!") + } + + time.Sleep(1 * time.Second) + g.Result.Print(g.outStream) +} diff --git a/kadai3-1/nagaa052/pkg/game/game_test.go b/kadai3-1/nagaa052/pkg/game/game_test.go new file mode 100644 index 0000000..c944348 --- /dev/null +++ b/kadai3-1/nagaa052/pkg/game/game_test.go @@ -0,0 +1,148 @@ +package game_test + +import ( + "bytes" + "io" + "log" + "reflect" + "testing" + + "github.com/gopherdojo/dojo5/kadai3-1/nagaa052/pkg/game" + "github.com/gopherdojo/dojo5/kadai3-1/nagaa052/pkg/questions" + qs "github.com/gopherdojo/dojo5/kadai3-1/nagaa052/pkg/questions" +) + +func TestResult_Print(t *testing.T) { + successOut := `======================== +Correct Count: 1 +Correct Rate: 50.0% +` + + type fields struct { + Questions []*qs.Question + CorrectCount int + } + tests := []struct { + name string + fields fields + wantOut string + }{ + { + name: "Success test", + fields: fields{ + Questions: []*qs.Question{&qs.Question{}, &qs.Question{}}, + CorrectCount: 1, + }, + wantOut: successOut, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &game.Result{ + Questions: tt.fields.Questions, + CorrectCount: tt.fields.CorrectCount, + } + out := &bytes.Buffer{} + r.Print(out) + if gotOut := out.String(); gotOut != tt.wantOut { + t.Errorf("Result.Print() = %v, want %v", gotOut, tt.wantOut) + } + }) + } +} + +func TestNew(t *testing.T) { + type args struct { + opt game.Options + inStream io.Reader + } + tests := []struct { + name string + opt game.Options + wantErr bool + }{ + { + name: "Success Test", + opt: game.DefaultOptions, + wantErr: false, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + inStream := &bytes.Buffer{} + outStream := &bytes.Buffer{} + errStream := &bytes.Buffer{} + + _, err := game.New(tt.opt, inStream, outStream, errStream) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func TestGame_getQuestion(t *testing.T) { + tests := []struct { + name string + opt game.Options + wantWord string + wantErr bool + }{ + { + name: "Success Test", + opt: game.DefaultOptions, + wantWord: "Hoge", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := testNewMock(t, tt.opt) + got, err := game.ExportGetQuestion(g) + if (err != nil) != tt.wantErr { + t.Errorf("Game.getQuestion() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got.Word, tt.wantWord) { + t.Errorf("Game.getQuestion() = %v, want %v", got, tt.wantWord) + } + }) + } +} + +type MockStore struct{} + +func (ms *MockStore) GetSize() int { + return 1 +} + +func (ms *MockStore) GetOne(index int) (*questions.Question, error) { + return &questions.Question{ + Word: "Hoge", + }, nil +} + +func testNewMock(t *testing.T, opt game.Options) *game.Game { + t.Helper() + + inStream := &bytes.Buffer{} + outStream := &bytes.Buffer{} + errStream := &bytes.Buffer{} + + g, err := game.New(opt, inStream, outStream, errStream) + if err != nil { + log.Fatal("Failed to generate game mock") + } + + mockStore, err := questions.NewWithStore(&MockStore{}) + if err != nil { + log.Fatal("Failed to generate store mock") + } + + g.ExportSetQs(mockStore) + return g +} diff --git a/kadai3-1/nagaa052/pkg/questions/export_test.go b/kadai3-1/nagaa052/pkg/questions/export_test.go new file mode 100644 index 0000000..f21810c --- /dev/null +++ b/kadai3-1/nagaa052/pkg/questions/export_test.go @@ -0,0 +1,5 @@ +package questions + +var ExportNewInMemQ = newInMemQ + +var ExportInMemWords = words diff --git a/kadai3-1/nagaa052/pkg/questions/memory.go b/kadai3-1/nagaa052/pkg/questions/memory.go new file mode 100644 index 0000000..86e13f9 --- /dev/null +++ b/kadai3-1/nagaa052/pkg/questions/memory.go @@ -0,0 +1,58 @@ +package questions + +import "errors" + +type inMemQ struct{} + +var words = []string{ + "Germany", + "Italy", + "Russia", + "Ukraine", + "Ireland", + "Iceland", + "Albania", + "Belgium", + "Bulgaria", + "Denmark", + "Estonia", + "Finland", + "France", + "Greece", + "Greenland", + "Hungary", + "Cyprus", + "Croatia", + "Macedonia", + "Malta", + "Monaco", + "Norway", + "Austria", + "Netherlands", + "Poland", + "Portugal", + "Romania", + "Switzerland", + "Spain", + "Sweden", + "Serbia", + "Montenegro", +} + +func newInMemQ() (*inMemQ, error) { + return &inMemQ{}, nil +} + +func (iq *inMemQ) GetSize() int { + return len(words) +} + +func (iq *inMemQ) GetOne(index int) (*Question, error) { + if len(words) <= index { + return nil, errors.New("Question was not found") + } + + return &Question{ + Word: words[index], + }, nil +} diff --git a/kadai3-1/nagaa052/pkg/questions/memory_test.go b/kadai3-1/nagaa052/pkg/questions/memory_test.go new file mode 100644 index 0000000..db37e07 --- /dev/null +++ b/kadai3-1/nagaa052/pkg/questions/memory_test.go @@ -0,0 +1,61 @@ +package questions_test + +import ( + "reflect" + "testing" + + q "github.com/gopherdojo/dojo5/kadai3-1/nagaa052/pkg/questions" +) + +func Test_inMemQ_GetOne(t *testing.T) { + imq, err := q.ExportNewInMemQ() + if err != nil { + t.Errorf("Failed new In memoruy quetions") + } + + type fields struct { + words []string + } + type args struct { + index int + } + tests := []struct { + name string + args args + want *q.Question + wantErr bool + }{ + { + name: "Success test", + args: args{ + index: 0, + }, + want: &q.Question{ + Word: q.ExportInMemWords[0], + }, + wantErr: false, + }, + { + name: "Question not found test", + args: args{ + index: 9999999, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := imq.GetOne(tt.args.index) + if (err != nil) != tt.wantErr { + t.Errorf("inMemQ.GetOne() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("inMemQ.GetOne() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/kadai3-1/nagaa052/pkg/questions/questions.go b/kadai3-1/nagaa052/pkg/questions/questions.go new file mode 100644 index 0000000..60d8d4d --- /dev/null +++ b/kadai3-1/nagaa052/pkg/questions/questions.go @@ -0,0 +1,40 @@ +package questions + +// Questions is Get a questions, etc. +type Questions struct { + Store +} + +// Store is interface for acquiring questions data +type Store interface { + GetSize() int + GetOne(index int) (*Question, error) +} + +var _ Store = &inMemQ{} + +// New is Initialize Questions +func New() (*Questions, error) { + imq, err := newInMemQ() + if err != nil { + return nil, err + } + return NewWithStore(imq) +} + +// NewWithStore is Questions initialization by specifying Store +func NewWithStore(s Store) (*Questions, error) { + return &Questions{ + Store: s, + }, nil +} + +// Question is have a Word +type Question struct { + Word string +} + +// IsCorrect is Determine if the questions and answers are correct +func (q *Question) IsCorrect(answer string) bool { + return q.Word == answer +} diff --git a/kadai3-1/nagaa052/pkg/questions/questions_test.go b/kadai3-1/nagaa052/pkg/questions/questions_test.go new file mode 100644 index 0000000..cc72c5d --- /dev/null +++ b/kadai3-1/nagaa052/pkg/questions/questions_test.go @@ -0,0 +1,152 @@ +package questions_test + +import ( + "fmt" + "log" + "math/rand" + "reflect" + "testing" + "time" + + "github.com/gopherdojo/dojo5/kadai3-1/nagaa052/pkg/questions" + qs "github.com/gopherdojo/dojo5/kadai3-1/nagaa052/pkg/questions" +) + +func Example() { + qs, err := questions.New() + if err != nil { + log.Fatal("Failed new quetions") + } + + rand.Seed(time.Now().UnixNano()) + index := rand.Intn(qs.GetSize()) + + q, err := qs.GetOne(index) + if err != nil { + log.Fatal("Failed get quetion") + } + + answer := q.Word + if q.IsCorrect(answer) { + fmt.Println("Correct!") + } + // Output: + // Correct! +} + +func TestNew(t *testing.T) { + + imq, err := qs.ExportNewInMemQ() + if err != nil { + t.Errorf("Failed new In memoruy quetions") + } + + tests := []struct { + name string + want *qs.Questions + wantErr bool + }{ + { + name: "Success test", + want: &qs.Questions{ + Store: imq, + }, + wantErr: false, + }, + } + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := qs.New() + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("New() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewWithStore(t *testing.T) { + type args struct { + s qs.Store + } + tests := []struct { + name string + args args + want *qs.Questions + wantErr bool + }{ + { + name: "Success test", + args: args{}, + want: &qs.Questions{ + Store: args{}.s, + }, + wantErr: false, + }, + } + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := qs.NewWithStore(tt.args.s) + if (err != nil) != tt.wantErr { + t.Errorf("NewWithStore() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewWithStore() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestQuestion_IsCorrect(t *testing.T) { + type fields struct { + Word string + } + tests := []struct { + name string + fields fields + answer string + want bool + }{ + { + name: "Is correct test", + fields: fields{ + Word: "HogeHoge", + }, + answer: "HogeHoge", + want: true, + }, + { + name: "Is not correct test", + fields: fields{ + Word: "FugaFuga", + }, + answer: "fugafuga", + want: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + q := &qs.Question{ + Word: tt.fields.Word, + } + if got := q.IsCorrect(tt.answer); got != tt.want { + t.Errorf("Question.IsCorrect() = %v, want %v", got, tt.want) + } + }) + } +}