diff --git a/kadai2/src/tomoyukikobayashi/testdata/file1.jpg b/kadai2/src/tomoyukikobayashi/testdata/file1.jpg new file mode 100755 index 0000000..7bb2715 Binary files /dev/null and b/kadai2/src/tomoyukikobayashi/testdata/file1.jpg differ diff --git a/kadai2/src/tomoyukikobayashi/testdata/file2.jpeg b/kadai2/src/tomoyukikobayashi/testdata/file2.jpeg new file mode 100755 index 0000000..7bb2715 Binary files /dev/null and b/kadai2/src/tomoyukikobayashi/testdata/file2.jpeg differ diff --git a/kadai2/src/tomoyukikobayashi/testdata/file3.png b/kadai2/src/tomoyukikobayashi/testdata/file3.png new file mode 100755 index 0000000..f8f2af7 Binary files /dev/null and b/kadai2/src/tomoyukikobayashi/testdata/file3.png differ diff --git a/kadai3-1/Makefile b/kadai3-1/Makefile new file mode 100644 index 0000000..0c454b8 --- /dev/null +++ b/kadai3-1/Makefile @@ -0,0 +1,25 @@ +.PHONY: deps +deps: + go get github.com/golang/lint/golint + +.PHONY: build +build: + go build -o type tomoyukikobayashi + +.PHONY: test +test: + go test -v -cover ./... + +.PHONY: cover +cover: + go test -coverprofile=mainprof tomoyukikobayashi + go tool cover -html=mainprof + +.PHONY: lint +lint: deps + go vet ./... + golint ./... + +.PHONY: fmt +fmt: deps + go fmt *.go diff --git a/kadai3-1/README.md b/kadai3-1/README.md new file mode 100644 index 0000000..1dbd149 --- /dev/null +++ b/kadai3-1/README.md @@ -0,0 +1,25 @@ +GoTypingGame +===== + +# Overview + +最高にいけてる楽しいタイピングゲーム +Goで書かれている + +# SetUp + +下記のようにコマンドを叩くと、実行形式のtypeファイルが生成されます +``` +make build +``` + +# Usage +``` +type [OPTION] +``` +オプション +``` +Usage of typing: + -t int + time to play (second) default=30s (default 30) +``` diff --git a/kadai3-1/src/tomoyukikobayashi/main.go b/kadai3-1/src/tomoyukikobayashi/main.go new file mode 100644 index 0000000..88138c3 --- /dev/null +++ b/kadai3-1/src/tomoyukikobayashi/main.go @@ -0,0 +1,102 @@ +/* +Pacakge main is the entry point of this project. +This mainly provides interaction logics and parameters +used in CLI intrerfaces. +*/ +package main + +import ( + "context" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "time" + + "tomoyukikobayashi/typing" +) + +const ( + wordFile = "words.yaml" +) + +// CLIのExitコード +const ( + ExitSuccess = 0 + ExitError = 1 + ExitInvalidArgs = 2 +) + +// Exitしてもテスト落ちない操作するようにエイリアスにしている +var exit = os.Exit + +// CLI テストしやすいようにCLIの出力先を差し替えられるようにしている +type CLI struct { + inStream io.Reader + outStream io.Writer + errStream io.Writer +} + +// CLIツールのエントリーポイント +func main() { + cli := &CLI{inStream: os.Stdin, outStream: os.Stdout, errStream: os.Stderr} + exit(cli.Run(os.Args)) +} + +// Run テスト用に実行ロジックを切り出した内容 +func (c *CLI) Run(args []string) int { + + flags := flag.NewFlagSet("typing", flag.ContinueOnError) + flags.SetOutput(c.errStream) + + var t int + flags.IntVar(&t, "t", 30, "time to play (second) default=30s") + + if err := flags.Parse(args[1:]); err != nil { + return ExitInvalidArgs + } + + // yamlファイルから語彙リストを読み出す + cur, _ := os.Getwd() + file, err := os.Open(filepath.Join(cur, wordFile)) + if err != nil { + fmt.Fprintf(c.outStream, "failed to initizalize game %v", err) + return ExitError + } + // クローズできなくても実害ないので、エラー処理は省略 + defer file.Close() + + // gameを動作させるインターフェイスを初期化 + game, err := typing.NewGame(file, c.inStream) + if err != nil { + fmt.Fprintf(c.outStream, "failed to initizalize game %v", err) + return ExitError + } + + fmt.Fprintf(c.outStream, "start game %d sec\n", t) + tc := time.Duration(t) * time.Second + ctx, cancel := context.WithTimeout(context.Background(), tc) + defer cancel() + qCh, aCh, rCh := game.Run(ctx) + + for { + q, progress := <-qCh + fmt.Fprintf(c.outStream, "%v\n", q) + if !progress { + break + } + + fmt.Fprintf(c.outStream, ">") + a, progress := <-aCh + fmt.Fprintf(c.outStream, "%v\n", a) + if !progress { + break + } + } + + r := <-rCh + fmt.Fprintf(c.outStream, "clear %v miss %v\n", r[0], r[1]) + + return ExitSuccess +} diff --git a/kadai3-1/src/tomoyukikobayashi/typing/game.go b/kadai3-1/src/tomoyukikobayashi/typing/game.go new file mode 100644 index 0000000..51c0a02 --- /dev/null +++ b/kadai3-1/src/tomoyukikobayashi/typing/game.go @@ -0,0 +1,108 @@ +package typing + +import ( + "bufio" + "context" + "io" +) + +// Game タイピングゲームを制御するロジックを返す +type Game interface { + Run(context.Context) (question <-chan string, answer <-chan string, result <-chan [2]int) +} + +type constGame struct { + Questioner + input io.Reader + // NOTE コンテキストにゲーム状態格納しようとしたがコピーして作り直すから、途中で書き換えてもルーチンをまたいでシェアされない(親子関係で) + // 結局共有変数に書いていることになるので、脆弱感がすごい + clear int + miss int + currentWord string +} + +// NewGame Gameのコンストラクタです +func NewGame(source, input io.Reader) (Game, error) { + // クイズデータを読み込む + d, err := NewQuizData(source) + if err != nil { + return nil, err + } + q := NewQuestioner(d) + return &constGame{Questioner: q, input: input}, nil +} + +// Run ゲームを開始する +func (c *constGame) Run(ctx context.Context) (<-chan string, <-chan string, <-chan [2]int) { + // TODO routine数にあわせてサイズ調整は死ねるのでonce.DO が使えるかも + routines := 2 + rCh := make(chan [2]int, routines) + + qCh := c.question(ctx, rCh) + aCh := c.answer(ctx, rCh) + + return qCh, aCh, rCh +} + +// 問題をqChに送る +func (c *constGame) question(ctx context.Context, rCh chan<- [2]int) <-chan string { + qCh := make(chan string) + go func() { + for { + word := c.GetNewWord(c.nextLevel()) + select { + case qCh <- word: + c.currentWord = word + case <-ctx.Done(): + rCh <- [2]int{c.clear, c.miss} + close(qCh) + return + } + } + }() + return qCh +} + +// 回答をストリームから読み込みしてaChに送る +func (c *constGame) answer(ctx context.Context, rCh chan<- [2]int) <-chan string { + sc := bufio.NewScanner(c.input) + aCh := make(chan string) + go func() { + for { + if !sc.Scan() { + continue + } + ans := sc.Text() + select { + case aCh <- ans: + if c.isCorrect(ans) { + c.clear = c.clear + 1 + } else { + c.miss = c.miss + 1 + } + // contextがtimeoutしたら結果を返却 + case <-ctx.Done(): + rCh <- [2]int{c.clear, c.miss} + close(aCh) + return + } + } + }() + return aCh +} + +// ワードの比較 +func (c *constGame) isCorrect(word string) bool { + return c.currentWord == word +} + +// HACK 成功した回数に応じて、使う語彙のレベルを決める。ここは決め打ちで書いてる +func (c *constGame) nextLevel() int { + if c.clear < 10 { + return 1 + } + if c.clear < 20 { + return 2 + } + return 3 +} diff --git a/kadai3-1/src/tomoyukikobayashi/typing/game_test.go b/kadai3-1/src/tomoyukikobayashi/typing/game_test.go new file mode 100644 index 0000000..67deba2 --- /dev/null +++ b/kadai3-1/src/tomoyukikobayashi/typing/game_test.go @@ -0,0 +1,125 @@ +package typing + +import ( + "context" + "io" + "os" + "strings" + "testing" + "time" +) + +func Test_NewGame(t *testing.T) { + tests := []struct { + name string + source io.Reader + hasErr bool + }{ + { + name: "valid", + source: strings.NewReader(` +Level1: +- hoge + +Level2: +- difficult + +Level3: +- test`), + hasErr: false, + }, + { + name: "invalid", + source: strings.NewReader(``), + hasErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewGame(tt.source, os.Stdin) + if err != nil && !tt.hasErr { + t.Fatalf("got err = %#v", err) + } + }) + } +} + +func Test_Run(t *testing.T) { + tests := []struct { + name string + source io.Reader + input io.Reader + hasErr bool + }{ + { + name: "valid", + source: strings.NewReader(` +Level1: +- hoge + +Level2: +- difficult + +Level3: +- testtestsete +`), + input: strings.NewReader(`hoge +hoge +hoge +hoge +hoge +hoge +hoge +hoge +miss +hoge +miss +hoge +difficult +difficult +`), + hasErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + game, err := NewGame(tt.source, tt.input) + if err != nil { + t.Fatalf("initialize game failed %v", game) + } + + tc := time.Duration(100) * time.Millisecond + ctx, cancel := context.WithTimeout(context.Background(), tc) + defer cancel() + + var want [2]int + qCh, aCh, rCh := game.Run(ctx) + var tmpQ, tmpA string + for { + select { + case a := <-aCh: + t.Errorf("a %v\n", a) + tmpA = a + // TODO 固まることがある + // TOOD 値が微妙に揺れる。。。 + if tmpQ == tmpA { + want[0] = want[0] + 1 + } else { + want[1] = want[1] + 1 + } + case q := <-qCh: + t.Errorf("q %v\n", q) + tmpQ = q + case got := <-rCh: + if want != got { + t.Fatalf("failed got = %v, want = %v", got, want) + } + default: + continue + } + } + }) + } +} diff --git a/kadai3-1/src/tomoyukikobayashi/typing/questions.go b/kadai3-1/src/tomoyukikobayashi/typing/questions.go new file mode 100644 index 0000000..35a95bb --- /dev/null +++ b/kadai3-1/src/tomoyukikobayashi/typing/questions.go @@ -0,0 +1,36 @@ +// Package typing このパッケージはタイピングゲームに関するロジックとデータを格納します +package typing + +import ( + "math/rand" + "time" +) + +// Questioner 質問で使うワードを提供します +type Questioner interface { + // GetNewWord 引数で与えた難易度に対して一つランダムにワードを返します + GetNewWord(level int) string +} + +type questionContainer struct { + qs map[int][]string +} + +// NewQuestioner Questionerのコンストラクタ +func NewQuestioner(data QuizData) Questioner { + qs := map[int][]string{} + for i := 1; i <= data.MaxLevel(); i++ { + qs[i] = data.WordsByLevel(i) + } + q := &questionContainer{ + qs: qs, + } + rand.Seed(time.Now().UnixNano()) + return q +} + +func (q *questionContainer) GetNewWord(level int) string { + rand := rand.Intn(len(q.qs[level])) + // HACK ほんとはmap okを見た方がいいけど、省略 + return q.qs[level][rand] +} diff --git a/kadai3-1/src/tomoyukikobayashi/typing/questions_test.go b/kadai3-1/src/tomoyukikobayashi/typing/questions_test.go new file mode 100644 index 0000000..e685c8b --- /dev/null +++ b/kadai3-1/src/tomoyukikobayashi/typing/questions_test.go @@ -0,0 +1,57 @@ +package typing + +import ( + "testing" +) + +type testData struct { +} + +func (td *testData) MaxLevel() int { + return 3 +} +func (td *testData) WordsByLevel(level int) []string { + switch level { + case 1: + return []string{"hoge", "foo", "baz"} + case 2: + return []string{"difficult", "anymatch", "haeeee"} + case 3: + return []string{"test"} + } + return []string{} +} + +func Test_GetNewWord(t *testing.T) { + q := testData{} + qst := NewQuestioner(&q) + + tests := []struct { + name string + level int + wantIn []string + }{ + { + name: "level1", + level: 1, + wantIn: q.WordsByLevel(1), + }, + { + name: "level2", + level: 2, + wantIn: q.WordsByLevel(2), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := qst.GetNewWord(tt.level) + for _, v := range tt.wantIn { + if got == v { + return + } + } + t.Fatalf("want = %#v, got = %#v", tt.wantIn, got) + }) + } +} diff --git a/kadai3-1/src/tomoyukikobayashi/typing/quiz_data.go b/kadai3-1/src/tomoyukikobayashi/typing/quiz_data.go new file mode 100644 index 0000000..ed182cb --- /dev/null +++ b/kadai3-1/src/tomoyukikobayashi/typing/quiz_data.go @@ -0,0 +1,50 @@ +package typing + +import ( + "io" + + yamler "gopkg.in/yaml.v2" +) + +// QuizData クイズデータを読み出す +type QuizData interface { + MaxLevel() int + WordsByLevel(int) []string +} + +// QuizSource HACK 使用しているパッケージの使用でしょうがなくpublicにしている +type QuizSource struct { + Level1 []string `yaml:"Level1,flow"` + Level2 []string `yaml:"Level2,flow"` + Level3 []string `yaml:"Level3,flow"` +} + +// NewQuizData クイズデータを生成する +func NewQuizData(source io.Reader) (QuizData, error) { + var s QuizSource + if err := yamler.NewDecoder(source).Decode(&s); err != nil { + return nil, err + } + + return &s, nil +} + +// MaxLevel クイズの最高難易度を返す +func (q *QuizSource) MaxLevel() int { + // HACK 決め打ち + return 3 +} + +// WordsByLevel 指定されたレベルの語彙を返す +func (q *QuizSource) WordsByLevel(level int) []string { + // HACK + switch level { + case 1: + return q.Level1 + case 2: + return q.Level2 + case 3: + return q.Level3 + } + return nil +} diff --git a/kadai3-1/src/tomoyukikobayashi/typing/quiz_data_test.go b/kadai3-1/src/tomoyukikobayashi/typing/quiz_data_test.go new file mode 100644 index 0000000..eafe3b1 --- /dev/null +++ b/kadai3-1/src/tomoyukikobayashi/typing/quiz_data_test.go @@ -0,0 +1,118 @@ +package typing + +import ( + "io" + "reflect" + "strings" + "testing" +) + +func Test_NewQuizData(t *testing.T) { + tests := []struct { + name string + reader io.Reader + want QuizSource + hasErr bool + }{ + { + name: "valid", + reader: strings.NewReader(` +Level1: +- hoge + +Level2: +- difficult + +Level3: +- test`), + want: QuizSource{ + Level1: []string{"hoge"}, + Level2: []string{"difficult"}, + Level3: []string{"test"}, + }, + hasErr: false, + }, + // TOOD 適当な文字列入れるとエラーなしでnil帰る + { + name: "invalidStruct", + reader: strings.NewReader(` +LevelA: +- hoge +`), + want: QuizSource{}, + hasErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewQuizData(tt.reader) + if err != nil && !tt.hasErr { + t.Fatalf("got err = %#v", err) + } + if !reflect.DeepEqual(got, &tt.want) { + t.Fatalf("want = %#v, got = %#v", tt.want, got) + } + }) + } +} + +func Test_MaxLevel(t *testing.T) { + q := QuizSource{ + Level1: []string{"hoge"}, + Level2: []string{"difficult"}, + Level3: []string{"test"}, + } + + // HACK 決め打ち + want := 3 + got := q.MaxLevel() + if want != got { + t.Fatalf("want = %d, got = %d", want, got) + } + +} + +func Test_WordsByLevel(t *testing.T) { + q := QuizSource{ + Level1: []string{"hoge"}, + Level2: []string{"difficult"}, + Level3: []string{"test"}, + } + + tests := []struct { + name string + level int + want []string + }{ + { + name: "level1", + level: 1, + want: []string{"hoge"}, + }, + { + name: "level2", + level: 2, + want: []string{"difficult"}, + }, + { + name: "level3", + level: 3, + want: []string{"test"}, + }, + { + name: "outOfRange", + level: 4, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := q.WordsByLevel(tt.level) + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("want = %#v, got = %#v", tt.want, got) + } + }) + } +} diff --git a/kadai3-1/words.yaml b/kadai3-1/words.yaml new file mode 100644 index 0000000..e9716b9 --- /dev/null +++ b/kadai3-1/words.yaml @@ -0,0 +1,17 @@ +Level1: +- hoge +- foo +- baz +- bar + +Level2: +- difficult +- omororororo +- feiajfiejawefa +- femiejaigfeag + +Level3: +- testewatawetawet +- gvewagweawegawe +- geawegawegawegawe +- gwageawegaweg \ No newline at end of file diff --git a/kadai3-2/Makefile b/kadai3-2/Makefile new file mode 100644 index 0000000..163737a --- /dev/null +++ b/kadai3-2/Makefile @@ -0,0 +1,26 @@ +.PHONY: deps +deps: + go get golang.org/x/sync/errgourp + go get github.com/golang/lint/golint + +.PHONY: build +build: + go build -o codehexisgod tomoyukikobayashi + +.PHONY: test +test: + go test -v -cover ./... + +.PHONY: cover +cover: + go test -coverprofile=mainprof tomoyukikobayashi + go tool cover -html=mainprof + +.PHONY: lint +lint: deps + go vet ./... + golint ./... + +.PHONY: fmt +fmt: deps + go fmt *.go diff --git a/kadai3-2/README.md b/kadai3-2/README.md new file mode 100644 index 0000000..c28c5e2 --- /dev/null +++ b/kadai3-2/README.md @@ -0,0 +1,19 @@ +CodeHexIsGod +===== + +# Overview + +CodeHex is God + +# SetUp + +下記のようにコマンドを叩くと、実行形式のgodehexisgodファイルが生成されます +``` +make build +``` + +# Usage +※Rangeダウンロードをサポートしているサイトに対してのみ、機能します +``` +codehexisgod [OPTION] URL +``` diff --git a/kadai3-2/src/tomoyukikobayashi/main.go b/kadai3-2/src/tomoyukikobayashi/main.go new file mode 100644 index 0000000..22a3487 --- /dev/null +++ b/kadai3-2/src/tomoyukikobayashi/main.go @@ -0,0 +1,185 @@ +/* +Pacakge main is the entry point of this project. +This mainly provides interaction logics and parameters +used in CLI intrerfaces. +*/ +package main + +import ( + "crypto/sha1" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + + "golang.org/x/sync/errgroup" +) + +// CLIのExitコード +const ( + ExitSuccess = 0 + ExitError = 1 + ExitInvalidArgs = 2 +) + +// HACK 雑に作ったので弱いバリデーション、パラメタ決め打ちと、処理されないエラーがある +// mainに全部押し込んで、テストもない + +// CLIツールのエントリーポイント +func main() { + if len(os.Args) < 2 { + fmt.Printf("missing URL parameter") + os.Exit(ExitInvalidArgs) + } + + url := os.Args[1] + fmt.Printf("start to download %v\n", url) + + /// Rangeアクセスの準備 + // HEADで取れた情報から、対象のAccept-Ranges対応確認と、ファイルサイズの取得 + hRes, err := http.Head(url) + if err != nil { + fmt.Printf("cannot get header %v from %v\n", err, url) + os.Exit(ExitError) + } + if _, ok := hRes.Header["Accept-Ranges"]; !ok { + fmt.Printf("range access does not supported") + os.Exit(ExitError) + } + + /// 処理するプロセス数を決める + // HACK とりあえず決めうち、本当は環境or引数からProc数決めるのが良い + procs := 4 + size, _ := strconv.Atoi(hRes.Header["Content-Length"][0]) + fmt.Printf("donwload size %v, parallel %v", size, procs) + + /// 一次ファイルを格納するフォルダの作成 + + // 一時ファイルを格納するディレクトリ名(とりあえずハッシュ)を決める + h := sha1.Sum([]byte(url)) + hash := fmt.Sprintf("%x", h) + + // DL共通のtmpディレクトリと、個別のDL用のhash名のディレクトリを掘る + cur, _ := os.Getwd() + // tmpフォルダがなければ作る + if _, err := os.Stat(filepath.Join(cur, "tmp")); os.IsNotExist(err) { + err := os.Mkdir("tmp", 0777) + if err != nil { + fmt.Printf("failed to create tmp file dir") + os.Exit(ExitError) + } + } + // tmp配下にハッシュにマッチするフォルダがなければ作る + dlTmp := filepath.Join("tmp", hash) + if _, err := os.Stat(filepath.Join(cur, dlTmp)); os.IsNotExist(err) { + err := os.Mkdir(dlTmp, 0777) + if err != nil { + fmt.Printf("failed to create tmp file dir") + os.Exit(ExitError) + } + } + + /// goroutine起こして並列ダウンロード + var eg errgroup.Group + // bytes のレンジに指定するバイト位置 + from, to := 0, 0 + for i := 0; i < procs; i++ { + // 各分割リクエストの取得バイト範囲を決める + to = from + size/procs + if i == procs-1 { + to = to + size%procs + } + + // 平行にDLをする + i := i + 1 + eg.Go(func() error { + fn := filepath.Join(dlTmp, strconv.FormatInt(int64(i), 10)) + + // ファイルが存在していたらスキップする + // TODO 本当はファイルサイズがto - fromと一致するかも見た方が良い + if _, err := os.Stat(fn); err == nil { + return nil + } + + // Rangeを指定してGETリクエスト作成 + dReq, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + dReq.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", from, to)) + + // DLリクエスト + dRes, err := http.DefaultClient.Do(dReq) + if err != nil { + return err + } + + // 分割ファイルの保存 + file, err := os.Create(fn) + if err != nil { + return err + } + defer file.Close() + + // TODO ここで落ちるとファイルが存在するけどDLできていないのにスキップしてしまう + _, err = io.Copy(file, dRes.Body) + if err != nil { + return err + } + + return nil + }) + + from = to + 1 + } + // 全プロセスがDL終わるまで待つ + if err := eg.Wait(); err != nil { + fmt.Printf("error occurred while downloading %v", err) + os.Exit(ExitError) + } + + /// DLした部分ファイルを結合する + // deferをos.Existと同じブロックに置かないように無名関数にしている + // (でもdefer使わないでシーケンシャルにやるのと変わらんなこれだと) + err = func() error { + // 複数ファイルを一気に読み出すReaderを作成 + files := make([]io.Reader, procs) + for i := 0; i < procs; i++ { + file, err := os.Open(filepath.Join(dlTmp, strconv.FormatInt(int64(i+1), 10))) + if err != nil { + fmt.Printf("error occurred while reading tmp file %v", err) + os.Exit(ExitError) + } + // 閉じるの失敗しても実害ないので、エラー処理なし + defer file.Close() + files[i] = file + } + reader := io.MultiReader(files...) + + /// tmpフォルダのゴミ掃除(失敗しても特に何もしない)) + defer os.RemoveAll(dlTmp) + + // ファイルを結合する + file, err := os.Create(filepath.Base(url)) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(file, reader) + if err != nil { + return err + } + + return nil + }() + + if err != nil { + fmt.Printf("error occurred while creating dl file %v", err) + os.Exit(ExitError) + } + + os.Exit(ExitSuccess) +}