数入れパズル「正くん」の解説

数入れパズル「正くん」は、以前に解法プログラムで取り上げた「正」を実際のパズルに 仕立てたものです。「正くん」パズルのルールは簡単なもので、下図のように各辺の数字の和が等しくなるように 10 個の数字を A から J に配置するだけです。 今回は解法自体は取り上げませので、興味のある方は解法プログラムの 「」をご覧ください。

   A---B---C               A + B + C = B + E + I
       |                             = D + H
   D   E--F                          = E + F
   |   |                             = G + H + I + J
G--H---I-----J

問題集の作成

解法プログラム「正」では 0 から 9 までの連続する 10 個の数字を使いましたが、 たくさんの解がありました。辺の合計値が 12 で6つの解、13 で6つの解で、合わせて 12 の解です。 これでは、実際のパズルとして出題するには、解が多すぎて不向きです。 解法プログラムの「正」の最後の方で触れていますが、他の 10 個の数字の組み合わせでは解が1つのものがあります。 そこで、次のようなプログラムを使って、解が1つのものがどれくらいあるか調べてみました。

use strict;
my @work;
comb(0 .. 21);

sub comb {
  my @list = @_;
  my $limit = $#list - (9 - @work);
  foreach my $i (0 .. $limit) {
    push @work, $list[$i];
    if (@work == 10) {
      # ここから解法プログラムを呼び出す
    } else {
      comb(@list[$i + 1 .. $#list]);
    }
    pop @work;
  }
}

上のプログラムは、0 から 21 の 22 の数字から 10 個の数字の組み合わせをすべて生成します。 生成される組み合わせ数は、646,646 (22 * 21 * 20 * 19 * 18 * 17 * 16 * 15 * 14 * 13 / 10!) になります。1つの組み合わせを生成したら、(少し修正した) 解法プログラム呼び出します。646,646 の組み合わせの中には、解のないもの、解がたくさんあるものとさまざまですが、解が1つだけものが 55,908 ありました。数としては十分ですので、これを問題集とすることにしました。 問題集のファイルは、次のような形式にしてあります。

6:2:7:5:12:3:0:10:1:4
6:4:7:3:12:5:0:14:1:2
3:8:4:6:2:13:0:9:5:1
6:4:8:3:13:5:0:15:1:2
2:9:4:10:0:15:1:5:6:3
・・・
9:13:15:21:18:19:7:16:6:8
9:13:16:21:18:20:6:17:7:8
9:14:15:21:18:20:7:17:6:8
9:14:16:21:19:20:7:18:6:8
10:14:15:21:19:20:7:18:6:8

1行に1つの問題が、10 個の数字をコロンで連結して格納してあります。 数字の並び順は A, B, C, ... の順で、これがそのまま正解となります。出題の際には、10 個の数字を次のように並べ替えて表示します。

(0, 1, 2, 3, 4, 5, 6, 7, 10, 12)
(0, 1, 2, 3, 4, 5, 6, 7, 12, 14)
(0, 1, 2, 3, 4, 5, 6, 8, 9, 13)
(0, 1, 2, 3, 4, 5, 6, 8, 13, 15)
(0, 1, 2, 3, 4, 5, 6, 9, 10, 15)
・・・
(6, 7, 8, 9, 13, 15, 16, 18, 19, 21)
(6, 7, 8, 9, 13, 16, 17, 18, 20, 21)
(6, 7, 8, 9, 14, 15, 17, 18, 20, 21)
(6, 7, 8, 9, 14, 16, 18, 19, 20, 21)
(6, 7, 8, 10, 14, 15, 18, 19, 20, 21)

問題の出題をファイルに格納されている順にした場合、似たような数字の組み合わせが 続くことになります。そこで、ランダムに並べ替えることにしました。使用したコードは簡単なもので、 配列に入れて並べ替えました。コードと、並べ替えた結果は以下のとおりです。

my @random;
while (@array) {
  my $i = int rand($#array + 1);
  push @random, splice(@array, $1, 1);
}
7:10:16:18:12:21:3:15:11:4      (3, 4, 7, 10, 11, 12, 15, 16, 18, 21)
5:1:16:14:19:3:0:8:2:12         (0, 1, 2, 3, 5, 8, 12, 14, 16, 19)
8:5:11:21:4:20:0:3:15:6         (0, 3, 4, 5, 6, 8, 11, 15, 20, 21)
9:4:17:19:16:14:1:11:10:8       (1, 4, 8, 9, 10, 11, 14, 16, 17, 19)
6:11:17:18:14:20:2:16:9:7       (2, 6, 7, 9, 11, 14, 16, 17, 18, 20)
・・・
7:4:9:19:14:6:0:1:2:17          (0, 1, 2, 4, 6, 7, 9, 14, 17, 19)
9:1:13:12:17:6:0:11:5:7         (0, 1, 5, 6, 7, 9, 11, 12, 13, 17)
2:6:16:5:14:10:0:19:4:1         (0, 1, 2, 4, 5, 6, 10, 14, 16, 19)
6:10:17:19:15:18:4:14:8:7       (4, 6, 7, 8, 10, 14, 15, 17, 18, 19)
2:12:21:18:16:19:3:17:7:8       (2, 3, 7, 8, 12, 16, 17, 18, 19, 21)

最後に、1行の文字数を揃えました。1行文字数で一番長いものが、改行 (euc-jp なので1バイト) を含めて 28 文字です。28 文字に満たないものは、先頭にスペースを付け加えました。 その理由は、CGI プログラムの解説のところで説明します。

CGI プログラム

#!/usr/bin/perl -wT
use strict;
use IO::File;

open IN, "path/acc_tad.dat" or die "Can't open acc_tad.dat: $!";    # path は仮の名
my $acc_no = <IN>;
chomp $acc_no; ++$acc_no;
open OUT, ">path/acc_tad.dat" or die "Can't open acc_tad.dat: $!";
print OUT "$acc_no\n";
close OUT or die "Can't close acc_tad.dat: $!";
$acc_no =~ s/(\d)(\d\d\d)(,|$)/$1,$2/ while $acc_no =~ /\d\d\d\d/;

my ($rank, $ques_no);
unless ($ENV{QUERY_STRING}) {
  $rank = 'low';
  $ques_no = int rand 55908;
} else {
  ($ques_no, $rank) = (split /[k=]/, $ENV{QUERY_STRING})[1,2];
  ++$ques_no;
  $ques_no = 0 if $ques_no == 55908;
}

my $pos = 28 * $ques_no;
sysopen(FILE, "path/tadashi.dat", O_RDONLY) or die "Can't open tadashi.dat: $!";
sysseek(FILE, $pos, 0);
sysread(FILE, my $line, 27);
close FILE;
$line =~ s/^\s*//;

my @ans = split /:/, $line;
my $sum = $rank eq 'low'? $ans[0] + $ans[1] + $ans[2] : '--';
my $all = join(', ', sort { $a <=> $b } @ans);
my ($used, $unused, @disp_ind);
if ($rank eq 'high') {
  $used = '';
  $unused = "$all";
} else {
  @disp_ind = (0, 9);
  splice @disp_ind, 1, 0, 4 if $rank eq 'low';
  my @unused = @ans;
  my @used;
  push @used, splice(@unused, $_, 1) foreach reverse @disp_ind;
  $used = join(', ', sort { $a <=> $b } @used);
  $unused = join(', ', sort { $a <=> $b } @unused);
}

my @disp_item;
foreach my $i (0 .. 9) {
  my $j = $i + 2;
  if (@disp_ind && grep /$i/, @disp_ind) {
    push @disp_item, qq{name="var_$i" value="$ans[$i]" onFocus="nextPos($j)"};
  } else {
    push @disp_item, qq{name="var_$i" value="" onChange="chkitem($j);"};
  }
}

print "Content-Type: text/html\n\n";

print "<html>\n";
print "<head>\n";
print "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=euc-jp\">\n";
print "<title>正くんパズル</title>\n";
print "<script type=\"text/javascript\">\n";
print "<!--\n";
print "var all = [$all], used = [$used], unused = [$unused];\n";
print "var wused = [$used], wunused = [$unused], stArg;\n";

print "function chkitem (i) {\n";
print "  var itemValue = document.forms[0].elements[i].value;\n";
print "  if (itemValue != '' && !(_itemOk(itemValue))) {\n";
print "    alert(\" 入力に間違いがあります\");\n";
print "    stArg = i;\n";
print "    setTimeout(\"document.forms[0].elements[stArg].focus()\", 500);\n";
print "    return;\n";
print "  }\n";
print "  setArray();\n";
print "  wused.sort( function (a, b) { return a - b; });\n";
print "  wunused.sort( function (a, b) { return a - b; });\n";
print "  document.forms[0].elements[0].value = ' ' + wused.join(', ');\n";
print "  document.forms[0].elements[1].value = ' ' + wunused.join(', ');\n";
print "  return;\n";
print "}\n";

print "function setArray () {\n";
print "  wused = []; wunused = [];\n";
print "  for (var i = 2; i <= 11; i++) {\n";
print "    var x = document.forms[0].elements[i].value;\n";
print "    if (x != '') {\n";
print "      wused.push(x);\n";
print "    }\n";
print "  }\n";
print "  outerloop:\n";
print "  for (var i = 0; i < all.length; i++) {\n";
print "    for (var j = 0; j < wused.length; j++) {\n";
print "      if (all[i] == wused[j]) {\n";
print "        continue outerloop;\n";
print "      }\n";
print "    }\n";
print "    wunused.push(all[i]);\n";
print "  }\n";
print "}\n";

print "function _itemOk (y) {\n";
print "  for (var i = 0; i < wunused.length; i++) {\n";
print "    if (wunused[i] == y) { return true; }\n";
print "  }\n";
print "  return false;\n";
print "}\n";

print "function initArray () {\n";
print "  wused = used; wunused = unused;\n";
print "}\n";

print "function nextPos (i) {\n";
print "  var j = i < 11 ? i + 1 : 2;\n";
print "  document.forms[0].elements[j].focus();\n";
print "  return;\n";
print "}\n";

print "function chkform (arg) {\n";
print "  if (arg.var_0.value == \"\" || arg.var_1.value == \"\" ||\n";
print "      arg.var_2.value == \"\" || arg.var_3.value == \"\" ||\n";
print "      arg.var_4.value == \"\" || arg.var_5.value == \"\" ||\n";
print "      arg.var_6.value == \"\" || arg.var_7.value == \"\" ||\n";
print "      arg.var_8.value == \"\" || arg.var_9.value == \"\") {\n";
print "    alert(\"入力もれがあります\");\n";
print "    return;\n";
print "  }\n";
print "  var v1 = (parseInt(arg.var_0.value) + parseInt(arg.var_1.value) + parseInt(arg.var_2.value));\n";
print "  var v2 = (parseInt(arg.var_1.value) + parseInt(arg.var_4.value) + parseInt(arg.var_8.value));\n";
print "  var v3 = (parseInt(arg.var_3.value) + parseInt(arg.var_7.value));\n";
print "  var v4 = (parseInt(arg.var_4.value) + parseInt(arg.var_5.value));\n";
print "  var v5 = (parseInt(arg.var_6.value) + parseInt(arg.var_7.value) + parseInt(arg.var_8.value) + parseInt(arg.var_9.value));\n";
print "  if (v1 != v2 || v1 != v2 || v1 != v3 || v1 != v4 || v1 != v5) {\n";
print "    alert(\"不正解!!\");\n";
print "    return;\n";
print "  }\n";
print "  alert(\"正解!!!\");\n";
print "  return;\n";
print "}\n";
print "//-->\n";
print "</script>\n";
print "</head>\n";

print "<body bgcolor=\"#ddeeee\">\n";

print "<h2 align=\"center\">数入れパズル<br>正くん</h2><hr>\n";

print "<form name=\"quest\" action=\"#\">\n";
print "<table align=\"center\" border=\"0\" cellpadding=\"3\">\n";
print "<tr><td><p align=\"right\">累積出題回数: $acc_no</p></td></tr>\n";
if ($rank eq 'low') {
  print  "<tr><th><tt>[</tt>初級<tt>]</tt></th></tr>\n";
} elsif ($rank eq 'middle') {
  print "<tr><th><tt>[</tt>中級<tt>]</tt></th></tr>\n";
} else {
  print  "<tr><th><tt>[</tt>上級<tt>]</tt></th></tr>\n";
}
print  "<tr><td><tt>sum:    $sum</tt></td></tr>\n";
print  "<tr><td><tt>all:    ($all)</tt></td></tr>\n";
print  "<tr><td><tt>used:   <input type=\"text\" name=\"used\" value=\" $used\" size=\"38\" onFocus=\"blur()\"></tt></td></tr>\n";
print  "<tr><td><tt>unused: <input type=\"text\" name=\"unused\" value=\" $unused\" size=\"38\" onFocus=\"blur()\"></tt></td></tr>\n";
print "</table><br><br>\n";

print "<table align=\"center\" border=\"0\" cellspacing=\"0\">\n";
print  "<tr><td> </td><td> </td>\n";
print    "<td><tt><input type=\"text\" $disp_item[0] size=2></tt></td>\n";
print      "<td><b>-</b></td>\n";
print    "<td><tt><input type=\"text\" $disp_item[1] size=2></tt></td>\n";
print      "<td>-</td>\n";
print    "<td><tt><input type=\"text\" $disp_item[2] size=2></tt></td>\n";
print      "<td> </td><td> </td></tr>\n";

print  "<tr><td> </td><td> </td><td> </td>\n";
print      "<td> </td><td align=\"center\">|</td><td> </td>\n";
print      "<td> </td><td> </td><td> </td></tr>\n";

print  "<tr><td> </td><td> </td>\n";
print    "<td><tt><input type=\"text\" $disp_item[3] size=2></tt></td>\n";
print      "<td> </td>\n";
print    "<td><tt><input type=\"text\" $disp_item[4] size=2></tt></td>\n";
print      "<td>-</td>\n";
print    "<td><tt><input type=\"text\" $disp_item[5] size=2></tt></td>\n";
print      "<td> </td><td> </td></tr>\n";

print  "<tr><td> </td><td> </td><td align=\"center\">|</td>\n";
print      "<td> </td><td align=\"center\">|</td><td> </td>\n";
print      "<td> </td><td> </td><td> </td></tr>\n";

print  "<tr><td><tt><input type=\"text\" $disp_item[6] size=2></tt></td>\n";
print      "<td>-</td>\n";
print    "<td><tt><input type=\"text\" $disp_item[7] size=2></tt></td>\n";
print      "<td>-</td>\n";
print    "<td><tt><input type=\"text\" $disp_item[8] size=2></tt></td>\n";
print      "<td>-</td><td align=\"center\">-</td><td>-</td>\n";
print    "<td><tt><input type=\"text\" $disp_item[9] size=2></tt></td></tr>\n";
print "</table><br>\n";

print "<table align=\"center\" border=\"0\" cellpadding=\"10\"><tr>\n";
print " <td><input type=\"button\" value=\"check\" onclick=\"chkform(this.form);\"></td>\n";
print " <td><input type=\"reset\" value=\"reset\" onclick=\"initArray()\"></td></tr></table>\n";
print "</form><hr>\n";

print "<form action=\"tadashi.cgi\" method=\"GET\">\n";
print "<table align=\"center\" border=\"0\" cellpadding=\"10\"><tr>\n";
print " <td>Next:</td>\n";
print " <td><input type=\"submit\" name=\"rank$ques_no\" value=\"low\"></td>\n";
print " <td><input type=\"submit\" name=\"rank$ques_no\" value=\"middle\"></td>\n";
print " <td><input type=\"submit\" name=\"rank$ques_no\" value=\"high\"></td>\n";
print "</tr></table>\n";
print "</form>\n";
print "<hr>\n";

print "<table align=\"center\" border=\"0\"><tr><td><pre style=\"line-height: 120%\">\n";
print "ルール: 各辺の数字の和を等しくするように、unused の中の\n";
print "        数字を空欄に配置します。\n\n";
print "     A-B-C          A + B + C = B + E + I\n";
print "       |                      = D + H\n";
print "     D E-F                    = E + F\n";
print "     | |                      = G + H + I + J\n";
print "   G-H-I---J\n\n";
print "   初級: 合計値と A, E, J の3つ数字を表示\n";
print "   中級: A, J の2つ数字を表示\n";
print "   上級: 10個の数字のみを表示\n";
print "         (上級の場合は、常に A と C は交換可能です。\n";
print "          また、G と J も同じです。)\n\n";
print "   JavaScript が有効な場合、check ボタンを押すと正解か\n";
print "   不正解かを表示します。\n\n";
print "</pre></td></tr></table>\n";

print "<table border=\"0\" width=\"50%\" align=\"center\"><tr>\n";
print " <td valign=\"top\" nowrap><p style=\"color: #aa0000\">注意: </p></td>\n";
print " <td><p style=\"line-height: 130%\">Shift + Tab で欄を移動する場合、\n";
print "  最初から配置済みの欄を飛び越して前の欄に移動できません。例えば、初級の場合に F \n";
print "  から D へ移動することはできません。マウスを使って移動してください。</p>\n";
print " <p style=\"line-height: 130%\">ブックマークする場合、URL\n";
print "  欄にクエリ文字列 (?以降の末尾の文字列) を含まない状態でしてください。\n";
print "  <br>  O   http://kumo.sakura.ne.jp/tadashi.cgi\n";
print "  <br>  X   http://kumo.sakura.ne.jp/tadashi.cgi?rank<b>xxxx</b>=low\n";
print "  <br>xxxx の数字は問題Noを表します。下の行でブックマークすると、\n";
print "  毎回同じ問題Noから出題されるようになってしまいます。</p></td></tr></table>\n";

print "<hr><table align=\"center\" border=\"0\"><tr><td>メインページ: \n";
print "<a href=\"http://www12.ocn.ne.jp/~kumo/\">Perl and Puzzle</a></td></tr></table>\n";
print "</body>\n";
print "</html>\n";

CGI プログラムの解説

出題画面と JavaScript

パズルでは、「初級」「中級」「上級」の3つのランクが設けてあります。 問題集の中から1つ選択して、それぞれのランクに応じて問題を出題します。 それぞれのランク用に問題を用意してあるわけではなく、表示形式を変えているだけです。 下の図は、「中級」問題の例です。

[中級]
sum:    --
all:    (0, 1, 2, 6, 8, 9, 10, 14, 15, 17)
used:   
unused:


   - -   
     |     
     -   
  |  |     
- - ---


Next:
  [rank]:   「初級」「中級」「上級」のランク
  sum:      「初級」のみ辺の合計値を表示
  all:       使用する 10 個の数字を表示
  used:      配置済みの数字を表示
  unused:    未配置の数字を表示


  check:     正解か不正解かの確認ボタン
  reset:     リセットボタン
  Next:      次の問題のリクエストボタン

ここで、出題画面で JavaScript が行っていることを説明することにします。とは言っても JavaScript は、試験勉強の一夜漬けと同じでにわかに学習したものです。JavaScript のコードを説明するには知識が不足しているので、今回はコードの細かな説明は省略します。なお、JavaScript のコードは HTML にベタ書きしているので、ブラウザの「ページのソース」で見ることができます。

問題 No 管理の仕組み

問題は、問題集のファイルから昇順で出題します。この場合、通常はサーバー側で何らかの問題 No を管理する仕組みが必要になりますが、今回の CGI ではサーバー側に負担のない方法を用いています。 その方法とは、問題 No を一時的にユーザー側に保存してもらう、というものです。

my ($rank, $ques_no);
unless ($ENV{QUERY_STRING}) {   # 初回のアクセス
  $rank = 'low';
  $ques_no = int rand 55908;
} else {                        # 「次の問題」でのアクセス
  ($ques_no, $rank) = (split /[k=]/, $ENV{QUERY_STRING})[1,2];
  ++$ques_no;
  $ques_no = 0 if $ques_no == 55908;
}
......

print "<form action=\"tadashi.cgi\" method=\"GET\">\n";
print "<table align=\"center\" border=\"0\" cellpadding=\"10\"><tr>\n";
print " <td>次の問題:</td>\n";
print " <td><input type=\"submit\" name=\"rank$ques_no\" value=\"low\"></td>\n";
print " <td><input type=\"submit\" name=\"rank$ques_no\" value=\"middle\"></td>\n";
print " <td><input type=\"submit\" name=\"rank$ques_no\" value=\"high\"></td>\n";
print "</tr></table>\n";
print "</form>\n";

ユーザーのアクセスが最初かそうでないかは、URL のクエリ文字列 (環境変数の $ENV{QUERY_STRING}) を見て判断します。ユーザーのアクセスが最初の場合は、ランクを「初級」に設定し、 1つの問題をランダムに選択します。そして、選択した問題 No は、「次の問題」リクエストボタンの name 属性に埋め込んでおきます。このようにしておくと、サーバー側の処理が終了して CGI のプロセスが消滅したとしても問題 No を保持しておくことができます。

2回目以降の「次の問題」リクエストボタンを押した場合は、URL の末尾に現在の問題 No を含むクエリ文字列が付加されます。CGI プログラムでは、現在の問題 No を取り出してインクリメントし、 出題の処理をしたあとに再び「次の問題」リクエストボタンの name 属性に問題 No を埋め込んでおきます。 このような仕組みを用いると、ユーザーごとに昇順に問題を出題することができます。

問題の読み取り

次は、上で設定された $ques_no の行を問題集のファイルから読み取ります。 通常のファイルから特定の行を読み取るには、先頭から特定の行までを読み飛ばすしかありません。 コードとしては、次のようなものです。

while (<FILE>) {
  if ($. == $ques_no) {
    $line = $_;
    last;
  }
}

ファイルの行数が少ない場合には上のコードでも十分ですが、問題集のファイルは 55,908 行あり効率が少々悪くなります。平均して1度に読む行は、27,954 行にもなってしまいます。 そこで、今回はファイルの読み取りを、低レベルのアクセスで対応することにしました。 問題集のファイルは、低レベルで読みとれるように、(「問題集の作成」の最後で触れているように) 1行の文字数を揃えてあります。以下は、問題集のファイルの一部 (説明の都合上、改行を表示) と読み取るためのコードです。

   3:16:7:15:8:18:0:11:2:13\n
 5:14:15:21:18:16:9:13:2:10\n
   4:16:9:18:10:19:7:11:3:8\n
    3:2:14:11:7:12:0:8:10:1\n
   2:12:11:10:9:16:0:15:4:6\n
    8:1:11:16:5:15:0:4:14:2\n
  8:1:21:14:19:11:0:16:10:4\n
     2:3:16:15:13:8:1:6:5:9\n
    8:3:11:15:6:16:0:7:13:2\n
10:16:13:18:20:19:1:21:3:14\n
my $pos = 28 * $ques_no;
sysopen(FILE, "path/tadashi.dat", O_RDONLY) or die "Can't open tadashi.dat: $!";
sysseek(FILE, $pos, 0);
sysread(FILE, my $line, 27);
close FILE;
$line =~ s/^\s*//;

問題集のファイルは1行が改行 (1バイト) を含めて 28 文字ですので、28 * $ques_no で出題する問題の先頭の位置を計算することができます (なお、問題 No は内部的なものなので、0 から数えます)。ファイルから読み取る位置を計算したら、ファイルを sysopen で開いて sysseek でポインタを移動して、sysread で改行の手前まで読み取ります。そして、(存在する場合は) 先頭の空白を削除します。

表示用データの生成と HTML の出力

ここまでで、$rank に low, middle, high のいずれかを、$line に出題する問題 (例えば 3:16:7:15:8:18:0:11:2:13) を取得しています。この2つの変数をもとに、表示用のデータを作成します。 表示用のデータには、sum, all, used, unused と「正」の各欄があります。初級のみ表示する sum は、$line の最初の3つの数字を加算することで得ることができます。all は $line を数値順に並べることで、used は初級と中級で配置済みの数字を、unused に残りの数字をセットします。「正」の各欄のデータは、 以下のコードで生成しています。

my @disp_item;
foreach my $i (0 .. 9) {
  my $j = $i + 2;
  if (@disp_ind && grep /$i/, @disp_ind) {   # 配置済み欄用
    push @disp_item, qq{name="var_$i" value="$ans[$i]" onFocus="nextPos($j);"};
  } else {                                   # 空欄用
    push @disp_item, qq{name="var_$i" value="" onChange="chkitem($j);"};
  }
}

ここで生成している文字列は、各欄のフォームで指定するもので @disp_item に格納しておきます。各欄の name は、var_ に数字を付加したものにしてあります。 配置済み欄用では、value に表示する数字と JavaScript のイベントハンドラ onFocus 用の関数 nextPos() を設定します。nextPos() は、配置済み欄にフォーカスがあたったときに、 フォーカスを次の欄に移動するための関数です。空欄用では、value に空文字を、入力された数字を検証するために onChange の関数として chkitem() を指定しています。

準備がすべて整ったので、残るは HTML の出力だけです。HTML の出力は、原始的な print 文を書き連ねるというスタイルで行っています。head 内に JavaScript のコードを含んでいるので、 非常に長くなっています。print 文だけで 167 行もあります。HTML では、table を多用して中央部に表示されるようにしています。内容的には難しいところはないので、ざっとご覧ください。

(2007/10/01)

TopPage