Skip to content

kadai3-1-torotake #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added kadai3-1/.gitkeep
Empty file.
2 changes: 2 additions & 0 deletions kadai3-1/torotake/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.vscode/
profile
42 changes: 42 additions & 0 deletions kadai3-1/torotake/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Gopher道場#6 課題3-1

## タイピングゲームを作ろう

* 標準出力に英単語を出す(出すものは自由)
* 標準入力から1行受け取る
* 制限時間内に何問解けたか表示する

### ビルド

```
$ go build -o game main.go
```

### Usage

```sh
game <options>

options
-f [問題文ファイルのパス] : 問題文ファイル (デフォルト:カレントディレクトリのwords.txt)
-t [制限時間(秒)] : 制限時間 (デフォルト:30秒)
```

#### 問題文ファイルについて

* 英単語を並べたテキストファイル
* 1行が1問となる
* 前後の空白はトリムされ、空行は無視される
* 問題はランダムに選択される (重複あり)

----

## テスト実行

```sh
$ go test -v github.com/gopherdojo/dojo6/kadai3-1/torotake/typing
=== RUN TestGame_Run
--- PASS: TestGame_Run (5.00s)
PASS
ok github.com/gopherdojo/dojo6/kadai3-1/torotake/typing 5.006s
```
70 changes: 70 additions & 0 deletions kadai3-1/torotake/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package main

import (
"bufio"
"flag"
"fmt"
"os"
"strings"
"time"

"github.com/gopherdojo/dojo6/kadai3-1/torotake/typing"
)

var (
optWordsPath string
optTimeout int
exitCode int
)

func init() {
// -f=[問題文ファイル] default : words.txt
// -t=[制限時間(秒)] default : 30
flag.StringVar(&optWordsPath, "f", "words.txt", "question words file path.")
flag.IntVar(&optTimeout, "t", 30, "timeout (s)")
flag.Parse()
}

func main() {
exec()
os.Exit(exitCode)
}

func exec() {
words, err := loadWords(optWordsPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Can't load question words file.\n")
}

timeout, _ := time.ParseDuration(fmt.Sprintf("%ds", optTimeout))
game := typing.Game{
Words: words,
Input: os.Stdin,
Output: os.Stdout,
Timeout: timeout,
}

game.Run()

fmt.Printf("Game over!\nYour score is %v\n", game.Score)
}

// 問題文ファイルの読み込み
// 1行が1つの問題。前後の空白はトリムし、空行は無視する。
func loadWords(path string) ([]string, error) {
fp, err := os.Open(path)
if err != nil {
return nil, err
}

words := make([]string, 0)
scanner := bufio.NewScanner(fp)
for scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
if len(text) > 0 {
words = append(words, text)
}
}

return words, nil
}
101 changes: 101 additions & 0 deletions kadai3-1/torotake/typing/typing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
Package typing はタイピングゲームを実装したパッケージです。
Game型に
・問題の文字列のリスト
・入力(デフォルトでは標準入力を想定)
・出力(デフォルトでは標準出力を想定)
・制限時間
をセットしてRun()を呼ぶことで実行します。
問題はリストからランダムで出題され、入力から同じ文字列が入力されると正解でスコアが1加算されます。
*/
package typing

import (
"bufio"
crand "crypto/rand"
"fmt"
"io"
"math"
"math/big"
"math/rand"
"time"
)

// Game 画像変換のオプション指定
type Game struct {
Words []string
Input io.Reader
Output io.Writer
Score int
Timeout time.Duration
}

// Reason ゲームの終了結果を表す型
type Reason int

const (
// Unknown 不明
Unknown Reason = iota
// Timeout タイムアウト
Timeout
// InputClosed 入力が閉じられた
InputClosed
)

// Run optionsに指定された内容に従って画像を変換します
func (t *Game) Run() Reason {
// 乱数シードの初期化
seed, _ := crand.Int(crand.Reader, big.NewInt(math.MaxInt64))
rand.Seed(seed.Int64())
// タイムアウト
timeoutCh := time.After(t.Timeout)

ch := inputCh(t.Input)
for {
fmt.Fprintf(t.Output, "Let's input : %v\n", t.getProblem())
select {
case text, ok := <-ch:
if !ok {
// 入力が閉じられたので終了
fmt.Fprintf(t.Output, "input closed\n")
return InputClosed
}
if t.match(text) {
t.Score++
fmt.Fprintf(t.Output, "...OK!\n\n")
} else {
fmt.Fprintf(t.Output, "...NG!\n\n")
}
case <-timeoutCh:
// タイムアウトで終了
fmt.Fprintf(t.Output, "timeout\n")
return Timeout
}
}
}

func (t *Game) getProblem() string {
index := rand.Intn(len(t.Words))
return t.Words[index]
}

func (t *Game) match(answer string) bool {
for _, v := range t.Words {
if answer == v {
return true
}
}
return false
}

func inputCh(r io.Reader) <-chan string {
lines := make(chan string)
go func() {
defer close(lines)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
lines <- scanner.Text()
}
}()
return lines
}
58 changes: 58 additions & 0 deletions kadai3-1/torotake/typing/typing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package typing_test

import (
"io/ioutil"
"strings"
"testing"
"time"

"github.com/gopherdojo/dojo6/kadai3-1/torotake/typing"
)

func TestGame_Run(t *testing.T) {
answer := "hoge\nfoo\nhoge\nbar\nhoge\n"
expectedScore := 3
expectedDuration := time.Duration(5 * 1000 * 1000 * 1000) // 5sec
durationMargin := time.Duration(500 * 1000 * 1000) // 500msはOKとする

// strings.NewReader(answer)だとすぐ全て読み取ってEOFで終わってしまうのでタイムアウトまで持たない
// ダミーで無限に改行を読み取り続けるio.Readerを使う
input := newInfiniteReader(answer)
startTime := time.Now()

g := typing.Game{
Words: []string{"hoge"},
Input: input,
Output: ioutil.Discard,
Timeout: expectedDuration,
}
g.Run()

duration := time.Since(startTime)
if duration < (expectedDuration-durationMargin) || duration > (expectedDuration+durationMargin) {
t.Errorf("Time error expected:(%v +- %v) actual:%v", expectedDuration, durationMargin, duration)
}

if g.Score != expectedScore {
t.Errorf("Score error expected:%d actual:%d", expectedScore, g.Score)
}
}

type InfiniteReader struct {
Src *strings.Reader
}

func newInfiniteReader(src string) *InfiniteReader {
return &InfiniteReader{strings.NewReader(src)}
}

func (r *InfiniteReader) Read(p []byte) (int, error) {
n, err := r.Src.Read(p)
if err != nil {
// 元文字列を読み切った後はEOFが来るので、ダミーの改行を読み取ったことにする
p[0] = '\n'
n = 1
err = nil
}
return n, err
}
Loading