演習7

演習6のプログラムを拡張して,時間制限を設けよ.また,キーボード入力時に音を鳴らすようにせよ.

解答例

  1. package main
  2. import (
  3. "time"
  4. "bufio"
  5. "os"
  6. "strconv"
  7. "os/exec"
  8. "./textbox"
  9. )
  10. const text = "Go言語はGoogleが開発した新しいプログラミング言語です."
  11. // --- ProblemBox ---
  12. // 問題文を0.5秒ごとに1文字ずつ表示するデータ型.
  13. type ProblemBox struct {
  14. *textbox.TextBox
  15. text []rune
  16. }
  17. // 位置,大きさ,表示する問題文を指定してProblemBoxを生成する.
  18. func NewPB(x,y,w,h int, s string) (pb *ProblemBox) {
  19. return &ProblemBox{textbox.New(x,y,w,h),[]rune(s)}
  20. }
  21. // 問題文を0.5秒ごとに表示する.
  22. // 問題文をすべて表示したら,チャネルendにtrueを送信し終了する.
  23. func (pb *ProblemBox) Play(end chan bool) {
  24. pb.Clear()
  25. for i:= 0; i < len(pb.text); i++ {
  26. pb.PutRune(pb.text[i])
  27. time.Sleep(5e8)
  28. }
  29. pb.Puts("[Enter]")
  30. end <- true
  31. }
  32. // --- ProblemBox end ---
  33. // --- InputBox ------------------------------------------------------
  34. // キーボード入力された文字を表示し,入力された文字と問題文を比較し,正誤判定するデータ型.
  35. type InputBox struct {
  36. *textbox.TextBox
  37. text []rune
  38. }
  39. // 位置,大きさ,問題文を指定してInputBoxを生成する.
  40. func NewIB(x,y,w,h int, s string) (ib *InputBox) {
  41. return &InputBox{textbox.New(x,y,w,h),[]rune(s)}
  42. }
  43. // 入力された文字を表示する.
  44. // 問題文と異なる文字が入力されたら,チャネルendにfalseを送信し終了する.
  45. // 問題文が正しく入力されたら,チャネルendにtrueを送信し終了する.
  46. func (ib *InputBox) Play(end chan bool) {
  47. keyin := bufio.NewReader(os.Stdin)
  48. stdout := bufio.NewWriter(os.Stdout)
  49. PLAY: for i:=0; i < len(ib.text)+1; i++ {
  50. c,_,_:= keyin.ReadRune()
  51. stdout.WriteByte('\a') // ベルを鳴らす
  52. stdout.Flush() // 出力バッファをフラッシュ(直ぐにベルを鳴らす)
  53. if c == '\n' {
  54. ib.Puts("[Enter]")
  55. } else {
  56. ib.PutRune(c)
  57. }
  58. if c == '\n' && i == len(ib.text) {
  59. end <- true
  60. break PLAY
  61. } else if i < len(ib.text) && c != ib.text[i] || c != '\n' && i == len(ib.text) {
  62. // 途中で間違えた || 最後に[Enter]以外の文字を押した
  63. end <- false
  64. break PLAY
  65. }
  66. }
  67. }
  68. // --- InputBox end --------------------------------------------------
  69. // --- CountdownBox --------------------------------------------------
  70. // 5秒前から1秒ごとにカウントダウンしながら残り秒数を表示するデータ型
  71. type CountdownBox struct {
  72. *textbox.TextBox
  73. count int
  74. }
  75. // 位置,大きさを指定してCountdownBoxを生成する.
  76. func NewCB(x,y,w,h,count int) (cb *CountdownBox) {
  77. return &CountdownBox{textbox.New(x,y,w,h),count}
  78. }
  79. // 5秒前から1秒ごとにカウントダウンしながら残り秒数を表示する.
  80. // 残り時間が0になったら,チャネルendにtrueを送信して終了する.
  81. func (cb *CountdownBox) Play(end chan bool) {
  82. PLAY: for {
  83. cb.Clear()
  84. if cb.count == 0 {
  85. cb.Puts("時間切れ!")
  86. end <- true
  87. break PLAY
  88. }
  89. cb.Puts("あと"+strconv.Itoa(cb.count)+"秒")
  90. cb.count--
  91. time.Sleep(1e9)
  92. }
  93. }
  94. // --- CoundownBox End ------------------------------------------------
  95. func main() {
  96. cmd := exec.Command("stty")
  97. cmd.Args = []string {"stty", "-echo", "cbreak"}
  98. cmd.Stdin = os.Stdin
  99. cmd.Run()
  100. defer func() {
  101. cmd = exec.Command("stty")
  102. cmd.Args = []string {"stty", "sane", "erase", "^H"}
  103. cmd.Stdin = os.Stdin
  104. cmd.Run()
  105. }()
  106. textbox.Clear()
  107. pb := NewPB(3,3,70,4, text)
  108. endPB := make(chan bool)
  109. go pb.Play(endPB)
  110. ib := NewIB(3,8,70,4, text)
  111. endIB := make(chan bool)
  112. go ib.Play(endIB)
  113. cb := NewCB(3,18,70,1,5)
  114. endCB := make(chan bool)
  115. mb := textbox.New(3,13,70,4)
  116. MAIN: for {
  117. select {
  118. case <- endPB:
  119. go cb.Play(endCB)
  120. case success := <- endIB:
  121. if success {
  122. mb.Puts("おめでとう! 成功です.")
  123. } else {
  124. mb.Puts("残念でした.キーが違います.")
  125. }
  126. break MAIN
  127. case <- endCB:
  128. mb.Puts("残念でした.時間切れです.")
  129. break MAIN
  130. }
  131. }
  132. }

解説

プログラムの構成と動作

  • 問題文を0.5秒ごとに1文字ずつ表示するデータ型ProblemBoxの他に,キーボード入力された文字を表示し,入力された文字と問題文を比較し,正誤判定するデータ型InputBoxと,5秒前から1秒ごとにカウントダウンしながら残り秒数を表示するデータ型CountDownBoxを定義する.14行目から38行目がProblemBox,40行目から78行目がInputBox,80行目から108行目がCountDownBox.
  • 126行目の go pb.Playで,問題文を0.5秒ごとに表示するゴルーチンを開始.130行目の go ib.Playで,文字入力と正誤判定を開始.問題文を表示するゴルーチンpb.Playが終了したら,go cb.Playで,カウントダウンのゴルーチンを開始.問題文を表示し終わってから,5秒で時間切れとなる.
  • 各ゴルーチンの終了とib.Playの終了状態をチャネルを使って取得し,それに応じたメッセージ("おめでとう! 成功です.","残念でした.キーが違います.","残念でした.時間切れです."のいずれか)を表示する(137行目から152行目).

チャネル

  • チャネルは,ゴルーチン間で,データをやり取りしたり,同期をとったりするための仕掛けです.より具体的には,先入れ先出し(FIFO,First In First Out)のデータ構造,キューです.すなわち,要素は送信された順に受信されます.
  • マップやスライスと同じくチャネルは参照型です.チャネルを作成するにはmakeを用います.例えば,ch := make(chan int) で整数型の要素をもつチャネルを生成します.また,第2引数(整数)を指定すると,チャネルのキャパシティ(バッファサイズ)を指定することができます.例えば,ch256 := make(chan int, 256) で,キャパシティが256のチャネルを作成します.
  • プログラムでは,125行目,129行目,133行目でbool型を要素とするキャパシティ0のチャネルを作成しています.
  • キャパシティがゼロより大きいときはチャネルは非同期となります.また,受信時には,バッファが空のときその受信はブロックされ,空でない時,即時に受信します.キャパシティがゼロまたは未指定のときは,送信と受信の準備がともに整ったときにのみ通信が完了します.
  • チャネルにデータを送信するには,二項演算子 送信ステートメント)でxの値がch256に送信されます.送信時には,バッファがいっぱいのとき,その送信はブロックされ,いっぱいでないとき,ブロックすることなく即時に送信します.
  • チャネルからデータを受信するには,受信演算子(単項演算子) チャネル型,スライス,マップ,チャネルの作成受信演算子送信ステートメントclose長さ,キャパシティメモリの割り当て,および,チュートリアルの素数,実践Go言語のチャネルを参照してください.

select

  • select文は通信可能な集合の中から,実行可能なものを選択し実行ます.switch文と似ていますが,select文のcase節では通信操作を行います.
  • プログラムでは,138行目~151行目で,endPB,endIB,endCBのいずれかのチャネルが受信可能なら,そのチャネルから要素を受信し,対応するcase節を実行します.どのチャネルも受信可能でなければ,チャネルのどれかが受信可能になるまでブロックします.
  • select文の詳細は,言語仕様を参照してください.

break

  • break文は,最も内側にあるfor,switch,select文の実行を終了します.
  • ラベルが指定されているときは,そのラベルが付けられたfor,switch,select文が終了します.
  • プログラムでは,147行目と150行目で break MAIN と指定しているので,内側のselect文ではなく,ラベルMAINが付けられた外側のfor文が終了します.
  • break文の詳細は言語仕様を参照してください.

音を鳴らす

  • VT100互換の端末エミュレータでは,BEL文字'\a'を出力することでビープ音を鳴らすことができます.
  • プログラムでは,60行目で,標準出力stdoutに'\a'を書き出すことで,ビープ音を鳴らしています.音を直ちに鳴らすために,61行目で出力バッファをフラッシュしています.