diff --git a/kadai3/README.md b/kadai3/README.md index bc31612..b248221 100644 --- a/kadai3/README.md +++ b/kadai3/README.md @@ -8,6 +8,16 @@ * 標準入力から1行受け取る * 制限時間内に何問解けたか表示する +遊び方 + +```sh +./1-typing +``` + +ゲームのルールを選択する画面が出るので、ターン制か時間制を選ぶ。 +Goのキーワードがランダムに出てくるので、タイプして[Enter]。 +入力との一致度によって得点が加算される。 + ## 課題3-2 分割ダウンローダーを実装しよう。 @@ -17,3 +27,12 @@ * エラー処理を工夫する * golang.org/x/sync/errgourpパッケージなどを使ってみる * キャンセルが発生した場合の実装を行う + +使い方 +```sh +./2-dler -n 3 -d ./download https://example.com/file +``` + +`-n`オプションはダウンロード時の分割数。デフォルトは2. +`-d`オプションはダウンロードしたファイルの保存先。デフォルトは`./download`. +Ctrl-Cでダウンロードを中断することができる。 diff --git a/kadai3/translucens/1-typing/typing.go b/kadai3/translucens/1-typing/typing.go new file mode 100644 index 0000000..3ac98b0 --- /dev/null +++ b/kadai3/translucens/1-typing/typing.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/translucens/dojo1/kadai3/translucens/1-typing/ui" +) + +func main() { + ui.MainScreen() +} diff --git a/kadai3/translucens/1-typing/typinggame/calcscore.go b/kadai3/translucens/1-typing/typinggame/calcscore.go new file mode 100644 index 0000000..25735be --- /dev/null +++ b/kadai3/translucens/1-typing/typinggame/calcscore.go @@ -0,0 +1,58 @@ +package typinggame + +// CalcScore caliculates player score based on player input +func CalcScore(correct, playerinput string) int { + editlen := EditLength(correct, playerinput) + correctlen := len(correct) + + if editlen > correctlen { + return 0 + } + + return correctlen - editlen +} + +// EditLength caliculates edit distance between two strings +func EditLength(str1, str2 string) int { + len1 := len(str1) + len2 := len(str2) + + str1 = " " + str1 + str2 = " " + str2 + + table := make([][]int, len1+1) + for i1 := range table { + table[i1] = make([]int, len2+1) + table[i1][0] = i1 + } + for i2 := range table[0] { + table[0][i2] = i2 + } + + for i1 := range table { + if i1 == 0 { + continue + } + + for i2 := range table[i1] { + if i2 == 0 { + continue + } + + cost := 0 + if str1[i1] != str2[i2] { + cost = 1 + } + table[i1][i2] = min(min(table[i1-1][i2]+1, table[i1][i2-1]+1), table[i1-1][i2-1]+cost) + } + } + + return table[len1][len2] +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} diff --git a/kadai3/translucens/1-typing/typinggame/calcscore_test.go b/kadai3/translucens/1-typing/typinggame/calcscore_test.go new file mode 100644 index 0000000..5e482d9 --- /dev/null +++ b/kadai3/translucens/1-typing/typinggame/calcscore_test.go @@ -0,0 +1,58 @@ +package typinggame + +import ( + "testing" +) + +func TestEditLength(t *testing.T) { + type args struct { + str1 string + str2 string + } + tests := []struct { + name string + args args + want int + }{ + {"same", args{"test String", "test String"}, 0}, + {"missing one char", args{"test String", "test Strin"}, 1}, + {"add one char", args{"test String", "test Stringg"}, 1}, + {"empty", args{"test String", ""}, 11}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := EditLength(tt.args.str1, tt.args.str2); got != tt.want { + t.Errorf("EditLength() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCalcScore(t *testing.T) { + type args struct { + correct string + userinput string + } + tests := []struct { + name string + args args + want int + }{ + {"correct", args{"string", "string"}, 6}, + {"small miss", args{"string", "strin"}, 5}, + {"small miss2", args{"string", "stringg"}, 5}, + {"middle miss", args{"string", "stri"}, 4}, + {"middle miss2", args{"string", "string12"}, 4}, + {"big miss", args{"string", ""}, 0}, + {"big miss2", args{"string", "string1234567"}, 0}, + {"short string", args{"go", "go"}, 2}, + {"short miss", args{"go", "g"}, 1}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := CalcScore(tt.args.correct, tt.args.userinput); got != tt.want { + t.Errorf("CalcScore() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/kadai3/translucens/1-typing/typinggame/problem.go b/kadai3/translucens/1-typing/typinggame/problem.go new file mode 100644 index 0000000..ef0d932 --- /dev/null +++ b/kadai3/translucens/1-typing/typinggame/problem.go @@ -0,0 +1,22 @@ +package typinggame + +import ( + "math/rand" + "time" +) + +func init() { + rand.Seed(time.Now().Unix()) +} + +var words = [...]string{"break", "default", "func", "interface", "select", + "case", "defer", "go", "map", "struct", + "chan", "else", "goto", "package", "switch", + "const", "fallthrough", "if", "range", "type", + "continue", "for", "import", "return", "var"} + +// RandomWord returns a random chosen word +func RandomWord() string { + + return words[rand.Intn(len(words))] +} diff --git a/kadai3/translucens/1-typing/ui/gamescreen.go b/kadai3/translucens/1-typing/ui/gamescreen.go new file mode 100644 index 0000000..0939b52 --- /dev/null +++ b/kadai3/translucens/1-typing/ui/gamescreen.go @@ -0,0 +1,179 @@ +package ui + +import ( + "bufio" + "fmt" + "io" + "os" + "time" + + "github.com/fatih/color" + "github.com/translucens/dojo1/kadai3/translucens/1-typing/typinggame" +) + +const ( + turncount = 5 + warmuptime = 3 + timeperchar = 1 + timetrialsec = 30 +) + +var ( + yellow = color.New(color.FgYellow).SprintFunc() + red = color.New(color.FgRed).SprintFunc() + blue = color.New(color.FgHiBlue).SprintFunc() + whiteBgcyan = color.New(color.FgHiWhite).Add(color.BgCyan).Add(color.Bold).SprintFunc() + whiteBggreen = color.New(color.FgHiWhite).Add(color.BgGreen).SprintfFunc() +) + +// high score and name + +// MainScreen is mode selector +func MainScreen() { + + fmt.Println(" ______ ______") + fmt.Println(" /_ __/_ ______ ___ / ____/___") + fmt.Println(" / / / / / / __ \\/ _ \\/ / __/ __ \\") + fmt.Println(" / / / /_/ / /_/ / __/ /_/ / /_/ /") + fmt.Println("/_/ \\__, / .___/\\___/\\____/\\____/") + fmt.Println(" /____/_/") + + strchan := strinput(os.Stdin) + defer close(strchan) + + for { + fmt.Println("Select game mode: ") + fmt.Printf("1: %d turns\n", turncount) + fmt.Printf("2: Timetrial %d sec.\n", timetrialsec) + fmt.Println("Other: Exit") + fmt.Print(whiteBgcyan(">>> ")) + + command, ok := <-strchan + switch { + case len(command) == 0 || !ok: + return + case command[0] == '1': + printScore(TurnGame(strchan)) + case command[0] == '2': + printScore(Timetrial(strchan)) + default: + return + } + + } + +} + +// TurnGame shows turn-ruled game screen for player +func TurnGame(strchan <-chan string) (int, int) { + + totalscore := 0 + totallength := 0 + + for i := warmuptime; i > 0; i-- { + fmt.Printf("%d...", i) + time.Sleep(time.Second) + } + + for i := 1; i <= turncount; i++ { + fmt.Print("Ready...") + time.Sleep(time.Second) + + fmt.Printf("Go!!\nTurn %d/%d\n", i, turncount) + word := typinggame.RandomWord() + lenstr, score := SingleTurn(strchan, word, time.Duration(len(word)*timeperchar)*time.Second) + + totallength += lenstr + totalscore += score + } + return totallength, totalscore +} + +// Timetrial shows time-based game screen +func Timetrial(strchan <-chan string) (int, int) { + + totallength, totalscore := 0, 0 + + for i := warmuptime; i > 0; i-- { + fmt.Printf("%d...", i) + time.Sleep(time.Second) + } + fmt.Print("Ready...") + time.Sleep(time.Second) + fmt.Println("Go!!") + + endAt := time.Now().Add(timetrialsec * time.Second) + + for endAt.After(time.Now()) { + + word := typinggame.RandomWord() + + lenstr, score := SingleTurn(strchan, word, endAt.Sub(time.Now())) + + totallength += lenstr + totalscore += score + } + + return totallength, totalscore +} + +func printScore(charcount, score int) { + fmt.Println() + fmt.Println(yellow(" ********************************** ")) + fmt.Printf("* Total Score: %d; Accuracy: %.1f%% *\n", score, float64(score)*100.0/float64(charcount)) + fmt.Println(yellow(" ********************************** ")) +} + +// SingleTurn shows typing object and returns word length and score +func SingleTurn(strchan <-chan string, correctstr string, timeout time.Duration) (int, int) { + + fmt.Println(whiteBggreen("### %s ### %d [sec.]", correctstr, timeout/time.Second)) + fmt.Print(">>> ") + + lenstr := len(correctstr) + + timer := time.NewTimer(timeout) + + for { + select { + case playerstr, ok := <-strchan: + + score := 0 + if ok { + score = typinggame.CalcScore(correctstr, playerstr) + + if score == lenstr { + fmt.Print(yellow("PERFECT! ")) + } else { + fmt.Print(red("miss... ")) + } + + fmt.Printf("Got %d point !\n", score) + } + timer.Stop() + + return lenstr, score + case _ = <-timer.C: + fmt.Printf("\nTimeup !!\n") + return lenstr, 0 + } + } + +} + +func strinput(r io.Reader) chan string { + ch := make(chan string) + go func() { + s := bufio.NewScanner(r) + for s.Scan() { + ch <- s.Text() + } + if err := s.Err(); err != nil { + fmt.Println(err.Error()) + } + // EOF + close(ch) + }() + + return ch +} diff --git a/kadai3/translucens/1-typing/ui/gamescreen_test.go b/kadai3/translucens/1-typing/ui/gamescreen_test.go new file mode 100644 index 0000000..56ec5d2 --- /dev/null +++ b/kadai3/translucens/1-typing/ui/gamescreen_test.go @@ -0,0 +1,93 @@ +package ui + +import ( + "strings" + "testing" +) + +func TestSingleTurn(t *testing.T) { + type args struct { + instr string + word string + } + tests := []struct { + name string + args args + wantStrlen int + wantScore int + }{ + {"perfect", args{"teststr", "teststr"}, 7, 7}, + {"miss1", args{"testst", "teststr"}, 7, 6}, + {"miss1", args{"test$tr", "teststr"}, 7, 6}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ch := make(chan string) + + returnch := make(chan int) + go func() { + strlen, score := SingleTurn(ch, tt.args.word) + returnch <- strlen + returnch <- score + }() + ch <- tt.args.instr + gotStrlen := <-returnch + gotScore := <-returnch + + if gotStrlen != tt.wantStrlen { + t.Errorf("SingleTurn() gotStrlen = %v, want %v", gotStrlen, tt.wantStrlen) + } + if gotScore != tt.wantScore { + t.Errorf("SingleTurn() gotScore = %v, want %v", gotScore, tt.wantScore) + } + }) + } +} + +func TestTimeup(t *testing.T) { + type args struct { + word string + } + tests := []struct { + name string + args args + wantStrlen int + wantScore int + }{ + {"timeout", args{"teststr"}, 7, 0}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ch := make(chan string) + + returnch := make(chan int) + go func() { + strlen, score := SingleTurn(ch, func() string { return tt.args.word }) + returnch <- strlen + returnch <- score + }() + gotStrlen := <-returnch + gotScore := <-returnch + + if gotStrlen != tt.wantStrlen { + t.Errorf("SingleTurn() gotStrlen = %v, want %v", gotStrlen, tt.wantStrlen) + } + if gotScore != tt.wantScore { + t.Errorf("SingleTurn() gotScore = %v, want %v", gotScore, tt.wantScore) + } + }) + } +} + +func TestStrInput(t *testing.T) { + + reader := strings.NewReader("teststring\n") + ch := strinput(reader) + + gotstring := <-ch + + if gotstring != "teststring" { + t.Errorf("strinput(r io.Reader) gotstring = %v", gotstring) + } + +} diff --git a/kadai3/translucens/2-dler/dler.go b/kadai3/translucens/2-dler/dler.go new file mode 100644 index 0000000..c1daee2 --- /dev/null +++ b/kadai3/translucens/2-dler/dler.go @@ -0,0 +1,60 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + + "github.com/translucens/dojo1/kadai3/translucens/2-dler/network" +) + +var ( + split int + savePath string + urls []string +) + +func init() { + const ( + defaultSplit = 2 + usageSplit = "specify download fragments count." + defaultSavePath = "./download" + usageSavePath = "specify file destination." + ) + + flag.IntVar(&split, "n", defaultSplit, usageSplit) + flag.StringVar(&savePath, "d", defaultSavePath, usageSavePath) +} + +func main() { + flag.Parse() + urls = flag.Args() + + if savePath[len(savePath)-1] != '/' { + savePath = savePath + "/" + } + + savePathStat, err := os.Stat(savePath) + if err != nil { + + if os.IsNotExist(err) { + if err := os.Mkdir(savePath, 0755); err != nil { + log.Fatalln(err.Error()) + return + } + } else { + log.Fatalln(err.Error()) + return + } + } else if !savePathStat.IsDir() { + fmt.Println("destination file exists") + } + + for _, url := range urls { + if err := network.Download(url, split, savePath); err != nil { + log.Fatalln(err.Error()) + } + } + +} diff --git a/kadai3/translucens/2-dler/network/controller.go b/kadai3/translucens/2-dler/network/controller.go new file mode 100644 index 0000000..62bbabe --- /dev/null +++ b/kadai3/translucens/2-dler/network/controller.go @@ -0,0 +1,206 @@ +package network + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "regexp" + "strconv" + "time" + + "golang.org/x/sync/errgroup" +) + +var ( + filenameRegexp = regexp.MustCompile(`([^/]+?)$`) + tempDir string +) + +func init() { + var err error + tempDir, err = ioutil.TempDir("", "dler") + if err != nil { + panic(err) + } +} + +type downloadResult struct { + downloadedBytes int64 + err error +} + +// GetFileSize returns file size of indicated by URL +func GetFileSize(url string) (int64, error) { + + res, err := http.Head(url) + if err != nil { + return 0, err + } + if res.Header.Get("Accept-Ranges") != "bytes" { + return 0, errors.New("this server does not support partial requests") + } + if res.StatusCode != 200 { + return 0, errors.New("File " + url + " is not available. HTTP: " + strconv.Itoa(res.StatusCode)) + } + + return res.ContentLength, nil +} + +func cutFileName(path string) string { + if path[len(path)-1] == '/' { + path = path[0 : len(path)-1] + } + + return filenameRegexp.FindString(path) +} + +// Download downloads specified files from URL +func Download(rawurl string, fragments int, savepath string) error { + defer CleanTempDir() + startAt := time.Now() + + parsedURL, err := url.Parse(rawurl) + if err != nil { + return err + } + filename := cutFileName(parsedURL.Path) + fileSize, err := GetFileSize(rawurl) + if err != nil { + return err + } + + fragmentIndices := make([]int64, fragments+1) + for i := range fragmentIndices { + fragmentIndices[i] = fileSize * int64(i) / int64(fragments) + } + + fragmentPaths := make([]string, fragments) + for i := range fragmentPaths { + fragmentPaths[i] = fmt.Sprintf("%s/%s.%d.tmp", tempDir, filename, i) + } + + ctx, cancel := context.WithCancel(context.Background()) + errg, ctx := errgroup.WithContext(ctx) + + signalCh := make(chan os.Signal, 1) + signal.Notify(signalCh, os.Interrupt) + go func() { + if sig, ok := <-signalCh; ok { + log.Println("recieved signal: " + sig.String()) + cancel() + } + return + }() + + for i := range fragmentPaths { + from := fragmentIndices[i] + to := fragmentIndices[i+1] - 1 + fragmentPath := fragmentPaths[i] + + errg.Go( + func() error { + select { + case <-ctx.Done(): + log.Println("Context canceled") + return ctx.Err() + default: + return DownloadFragment(ctx, rawurl, fragmentPath, from, to) + } + }) + } + if err := errg.Wait(); err != nil { + close(signalCh) + cancel() + CleanTempDir() + return err + } + close(signalCh) + + spentTime := float32((time.Now().Sub(startAt))/time.Millisecond) / 1000 + Mbps := float32(fileSize) * 8 / spentTime / 1000000 + fmt.Printf("Total: %d bytes (%.2f sec. / %.3f Mbps)\n", fileSize, spentTime, Mbps) + return Concatenate(fragmentPaths, savepath+filename) +} + +// DownloadFragment download specified URL in the range +// returns downloaded bytes +func DownloadFragment(ctx context.Context, url, filepath string, from, to int64) error { + + fmt.Printf("Downloading %d - %d: %s\n", from, to, filepath) + startAt := time.Now() + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + req = req.WithContext(ctx) + + if from < to { + req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", from, to)) + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + bytes, err := WriteFile(filepath, res.Body, os.O_WRONLY|os.O_TRUNC|os.O_CREATE) + + if bytes != to-from+1 { + return fmt.Errorf("file size does not match with expected %d byte; actual %d byte", to-from+1, bytes) + } + + spentTime := float32((time.Now().Sub(startAt))/time.Millisecond) / 1000 + Mbps := float32(bytes) * 8 / spentTime / 1000000 + fmt.Printf("Downloaded %d - %d (%.2f sec. / %.3f Mbps)\n", from, to, spentTime, Mbps) + return nil +} + +// WriteFile writes content to filepath +func WriteFile(filepath string, content io.Reader, fileflag int) (int64, error) { + + ofd, err := os.OpenFile(filepath, fileflag, 0755) + if err != nil { + return 0, err + } + defer ofd.Close() + + buffered := bufio.NewWriter(ofd) + defer buffered.Flush() + + return io.Copy(buffered, content) +} + +// Concatenate combines source files into one destination file +func Concatenate(srcs []string, dst string) error { + + if _, err := os.Stat(dst); err == nil { + now := time.Now().Unix() + dst = fmt.Sprintf("%s.%d", dst, now) + } + + for _, src := range srcs { + reader, err := os.Open(src) + if err != nil { + return err + } + _, err = WriteFile(dst, reader, os.O_WRONLY|os.O_APPEND|os.O_CREATE) + if err != nil { + return err + } + } + + return nil +} + +// CleanTempDir removes all temp files +func CleanTempDir() { + os.RemoveAll(tempDir) +} diff --git a/kadai3/translucens/2-dler/network/controller_test.go b/kadai3/translucens/2-dler/network/controller_test.go new file mode 100644 index 0000000..a176d8f --- /dev/null +++ b/kadai3/translucens/2-dler/network/controller_test.go @@ -0,0 +1,81 @@ +package network + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +const ( + PathFound = "found" + PathWithoutContentLength = "withoutcontentlength" + PathNotFound = "notfound" +) + +var testCondition int + +var testhandler = http.HandlerFunc( + func(writer http.ResponseWriter, req *http.Request) { + + switch testCondition { + case 0: + http.ServeFile(writer, req, "../testdata/1024") + case 1: + writer.WriteHeader(http.StatusOK) + case 2: + http.NotFound(writer, req) + } + }) + +func TestGetFileSize(t *testing.T) { + tests := []struct { + name string + condition int + want int64 + wantErr bool + }{ + {"200OK", 0, 1024, false}, + {"200OKwithoutContentLength", 1, -1, false}, + {"404NotFound", 2, 0, true}, + } + + testserver := httptest.NewServer(testhandler) + defer testserver.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + testCondition = tt.condition + got, err := GetFileSize(testserver.URL) + if (err != nil) != tt.wantErr { + t.Errorf("GetFileSize() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetFileSize() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_cutFileName(t *testing.T) { + type args struct { + url string + } + tests := []struct { + name string + args args + want string + }{ + {"withdirectories", args{"/a/b/file.txt"}, "file.txt"}, + {"nodir", args{"/file.htm"}, "file.htm"}, + {"endsdir", args{"/a/dir/"}, "dir"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := cutFileName(tt.args.url); got != tt.want { + t.Errorf("cutFileName() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/kadai3/translucens/README.md b/kadai3/translucens/README.md new file mode 100644 index 0000000..e69de29