わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

2重ループ養成CUI・nlmateをリリース

昨日,「nlmate」というRubyスクリプトを,http://github.com/takehiko/nlmateにて公開しました.
あらかじめ必要なのは,このスクリプトファイルのほか,rubyとccのコマンドです.ruby nlmateを実行すると,はじめに以下の表示が出てきます*1

[Tutorial]
"nlmate" is a game for you to solve the nested loop mating problem,
just like the Shogi mating problem (a.k.a. Tsume-Shogi).

Your task is typing in two "for statements" which work as
the nested loop to conform to the goal sequence. After the entry,
the fixed codes surround and enclose your instructions and the
source file is automatically compiled to decide if the output is
the same as the goal.

The input which begins with the word other than "for" is not
allowed, except that you may "quit" to stop playing the game.

All you have to do for the first task is punch in
"for (i = 0; i <= 2; i++)"
and
"for (j = 0; j <= 2; j++)"
respectively before hitting Enter key.

[Level 1]
Goal: (0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(2,2),
Outer loop:

「Outer loop:」の横で,入力ができます.ここに「for (i = 0; i <= 2; i++)」と打ち込み,Enterを押します.すると今度は「Inner loop:」と出ます.「for (j = 0; j <= 2; j++)」を打ち込んで*2,Enterキーです.「[Level 1]」のところからの出力は,こうなります.

[Level 1]
Goal: (0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(2,2),
Outer loop: for (i = 0; i <= 2; i++)
Inner loop:   for (j = 0; j <= 2; j++)
========================================
#include <stdio.h>

int main(void)
{
  int i = 123, j = 456;

  for (i = 0; i <= 2; i++)
    for (j = 0; j <= 2; j++)
      printf("(%d,%d),", i, j);

  return 0;
}
========================================
Output: (0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(2,2),
Goal:   (0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(2,2),
Correct!

[Level 2]
Goal: (0,0),(0,1),(0,2),(1,1),(1,2),(2,2),
Outer loop:

Level 1の問題は正解となりました.どんなコードで動作確認をしたかが,イコール記号の並びに挟まれて,出てきます.そして,このコードをもとにした出力と,Goalの内容とを,見比べることが可能です.
1問の流れは,「提示されるGoalを見て」「2つのfor文を入力し」「(nlmateが自動的に)前後にコードをつけてCファイルを作ってコンパイルして実行ファイルを作り」「実行結果が,Goalと等しいかどうか判定する」です.コンパイルエラーや,出力の無限ループ,結果の間違いには,それ相応のメッセージが出力され,同じ問題をやり直します.正解すれば,次のLevelに進みます.Inner loopのプロンプトが出ているときに,Outer loopの入力をやり直したいと思っても,できません(for,そして[Enter]を打って,最初からやり直しです).ただし,quit[Enter]とすれば,プログラムを終了します.
Levelは9まであります.現在の総問題数は15個です.あるLevelは2問の中から1問が選ばれ,あるLevelは問題(Goal)が固定です.
「Goalが固定」というのは厳密ではなく,若干のバリエーションがあります.スクリプトファイルに入っているGoalの値は,0オリジンで,わずかな例外を除いて,整数値は0,1,2を対象としています.バリエーションというのは,ユーザ指定の値(なければ乱数)に基づいて,0オリジンのところを1オリジンにするよう,一方または両方の成分へ一律に1を加えたり,成分の2つの値をすべて交換したりしていることを言います.出力の成分について,第1成分がiの値,第2成分がjの値なのは常に変わりませんので,成分をひっくり返すためには,2重ループの内側・外側の変数を交換しなければなりません.解答側は丸暗記というのはダメで,考えながら,正しく動く2重ループのコードを書いてもらうことになります.

このnlmateを書いた動機は,「学生は1年でCの授業を受けているのだけど,2年生になると,for文すら書けなくなっている」「4年生もだよ」といった,学科の何人かの先生の嘆きの声です.
「for文が書けないというのを,どう判定すればいいか?」「for文の書き方を忘れてしまった人が,思い出せるようにするために,何かサポートできないか?」と考え,Rubyのreadlineライブラリ*3を使えば,わりと簡単に作れるんじゃないかと思ったのでした.
コードは362行(空行・コメントを含む),総時間数は6時間程度ですが,いろいろ苦労しました.はじめに時間をとって,全体計画と,コンテンツに相当する問題(Goal)のデータを,テキストファイルに書き出しました.
Rubyスクリプトを実行する中で,Cのファイルをコンパイルしたり実行したりするというのは,演習科目の検証ツールで作成経験があり,open3ライブラリのOpen3.popen3を使用しました.これとtimeoutライブラリの組み合わせが,有効になっているのか,実はいいデータ入力が思い浮かんでいません.
今回も,Ruby 1.8,1.9両対応を心がけました.文字コードについては,「日本語を一切使わない」という解決策をとりました.
それでも1.8と1.9で互換性のない箇所がありました.具体的には,文字列sの先頭文字の文字コードを求める処理です.1.8ではs[0],1.9だとs.ordと書き,それぞれ反対のバージョンのrubyコマンドではエラーとなります.ただ,1.8でStringクラスにordインスタンスメソッドが定義されていないことに気づき,

unless String.instance_methods.include?(:ord)
  class String
    def ord
      self[0]
    end
  end
end

と書くことで,s.ordに一本化することができました.
結局,Rubyのバージョンについては,CygwinLinuxで/usr/bin/rubyに入っている1.8.7,スナップショットを取得してソースからビルドした1.8.8や1.9.2で,動作確認をしました.

各Levelの問いは,実際のプログラミングで使用するとは限りません.パズル的要素もあります.最初と最後のLevelを比較してみましょう.

[Level 1]
Goal: (0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(2,2),

なのに対して

[Level 9]
Goal: (0,0),(0,1),(0,2),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2),

です.先頭から4番目と6番目を交換してできる系列ですが,難易度はまったくと言っていいほど違います*4.この問題を含め,いくつかのLevelを解くには,「三項演算子」か「論理値としての評価は真が1,偽が0」のいずれかを使うことが必要となります.
とはいえ,2重ループの外側と内側のループでどうなればいいかをよく考えれば,どの問題も正解にたどり着けますし,このコマンドの特長は,教師なしで正解不正解がチェックできる点にあります.そこで,学生の自習用としてこのコマンドを使ってもらえるよう,今後も磨き上げを図っていきたいと考えています.
問題を充実させるだけでなく,時間を計って出力したりログファイルに残したりする機能を盛り込み,学生から協力者を募ってデータをもらうことなども,進めていきたいのですけどね.
最後に一つ,小技です.チュートリアルのメッセージが出て「Outer loop:」のプロンプトが出たときに,上矢印,上矢印,Enter,上矢印,上矢印,Enterとすれば,コードを一切打ち込むことなく,正解となります.Rubyのreadlineライブラリは,コマンド履歴が使えるので,プログラム内部で,正解となる2行の入力を,履歴にpushしているのでした.もちろんチュートリアル(Level 1)限定です.

*1:「nested loop mating problem」は,今回のリリースのために作った造語です.詰め将棋の訳語として「Shogi mating problem」は,英辞郎に載っていました.

*2:自動で字下げをします.内部事情を書いておきますと,「Outer loop:(空白1文字)」と「Inner loop:(空白3文字)」というプロンプトを与えて,入力を取得しているのでした.

*3:http://doc.okkez.net/static/191/class/Readline.html

*4:私自身がLevel 9をやっても,一発で正解するのは2割くらいです.