42 Tokyoでは課題のことをプロジェクトと呼ぶらしい。ここでは日々の課題とは別に課される、特別な課題をプロジェクトとする。プロジェクト資料を読み解くと、Piscineには3種類のプロジェクトが存在するようだ。
- 個人プロジェクト
- グループ・プロジェクト
- 最終プロジェクト
今回、採り上げるSastantuaは、どうやら最初の個人プロジェクトのようだ。問題の形式はロジック問題だが、その詳細を示さない。プログラム上定める必要のある仕様策定を受講生に委ねている節がある。
今回はアプローチ、考え方を示すのにサンプル・コードを併記した説明が分かりやすいだろうと考えた。サンプル・コードはPythonで記述している。Piscine受講生はアプローチからロジックまでを真似することはできるが、コードは自分で考えて書き直す必要がある。
ちなみにライブラリは使用していない。自動的にFloatになってしまう箇所だけ、intのキャスト関数を用いた。それ以外には、関数も用いていない。
準備
Pythonに限らず、言語や開発ツールのインストール不用で、直接オンラインでコードを実行することのできる環境が用意されている。例えば次のサイトだ。サンプル・コードをコピーし、試行錯誤しながら参照してほしい。
paiza.io
Sastantua
入力された「段数」に基づいて、次のようなピラミッドを描画せよ、ということだ。
何らかのロジックを見出し、ピラミッドの天辺から一気に描いていくのがスマートだが、丁寧に順番に取り組んでみようと思う。私は次のようにアプローチすることにした。
- 普通のピラミッドを考える。
- 普通のピラミッドを描画する。
- 普通のピラミッドの描画を一部スキップする。
- つまりSastantuaを描画する。
- 最下段にドアを描く。
まず段は考慮しない。あえて普通のピラミッドを考えてみる。普通のピラミッドが描けたならば、その途中の描画を飛ばすことでSastantuaを再現できないか、と考えたからだ。
ピラミッドを考える
ロジック問題に取り組むのと同様、手ごろなサイズのピラミッドをExcelに描いてみる。何らかのロジック、規則性を見出せないだろうか。私は高さ(行数)30のピラミッドを書いてみた。
気づいたのは、ピラミッドを構成するブロックの数(*の数)は、行数の2倍-1、ということだ。
指定された高さ(行数)のピラミッドを描くには、まず描画領域を定める必要がある。指定された高さが、描画領域の高さになる。描画領域の底辺は最大行数の2倍-1だ。
指定された行数分の繰り返し処理で、各行を生成する。各行には次の3要素が含まれている。
- 左側のスペース
- ピラミッドのブロック
- 右側のスペース
ピラミッドのブロック数は前述の規則性から分かる。左右のスペースは描画領域の底辺からブロック数を引いた数、さらにブロック左右のスラッシュ、バックスラッシュに相当する2文字分を差し引いて、その半分を割り当てればよい。
ちなみに各行に挿入されているy、'\t'は、デバッグ用に繰り返し回数(行数)と整形用のタブだ。
(参照:末尾コード1)
段数を考慮する-段数から行数を考える
Sastantuaでは行数ではなく、段数が指定される。指定された段数を行数に読みかえる必要がある。サンプル図を観察し、そこから得られる情報をまとめ、段数と行数の関係を考察する。
入力された段数 | 1 | 2 | 3 | 4 | 5 | 6 |
行の段数 | 3 | 4 | 5 | 6 | 7 | 8 |
Sastantua描画行数 | 3 | 7 | 12 | 18 | 25 | 33 |
スキップ行数 | 0 | 2 | 2 | 3 | 3 | 4 |
スキップ行数集計 | 0 | 2 | 4 | 7 | 10 | 14 |
ピラミッド描画行数 | 3 | 9 | 16 | 25 | 35 | 47 |
段数と格段の行数の関係はシンプルだ。段数+2。プログラムは0スタートなので、+1の補正が必要だ。
Sastantuaはピラミッド描画の途中をスキップしたものと考えているので、スキップすべき行数を定める必要がある。スキップ行数は少々トリッキーだ。明確な規則性は分からないが、どうやら002233445566...と続いている印象だ。それは段差に含まれる'*'の数で分かる。ここでは、これを仕様とする。最初の0は計算できないので、その個所だけ条件分岐する。該当段数が1以降の場合、該当段数-1と2の商に2を加えると、2233445566...となる。
スキップ行が存在するため、Sastantuaは本来描くべきピラミッドよりも低い。しかし、その底辺は本来のピラミッドの高さに基づく必要がある。Sastantuaの高さと、本来のピラミッドの高さの両方を知る必要がある。
- Sastantuaの行数=各段の行数の総和
- ピラミッドの行数=格段の行数+スキップ行の総和
ここまでの情報から、まず段数入力に基づいてピラミッドを描画してみる。コード1から2での、ロジックに変化に注目してほしい。段数から行数を求めるロジックを追加しただけで、描画ロジックは何も変更していない。
(参照:末尾コード2)
段数を考慮する-Sastantuaを描画する
段数に基づいて本来のピラミッドが描画できた。このロジックに基づいて、適切な行の描画をスキップすれば、Sastantuaが描画できるはずだ。適切な行をスキップするには、描画の繰り返し処理の行数を操作すればよい。例えばこんな感じだ。
- 特定段の開始行から終了行まで描画する。
- 次段(A)の開始行=前段の終了行+スキップ行数
- 次段(A)の終了行=次段(A)の開始行+次段(A)の行数
プログラムは0スタートなので、ここでも+1の補正が必要だ。
また、この処理は2段目以降を対象にする。スキップ行数は最初の0を計算できなかったからだ。
描画処理のロジックには手を加えない。ただ繰り返し処理に開始行、終了行を反映するだけだ。
今回、段ごとに描画を繰り返すため、描画領域の高さは定めていない。ピラミッドの高さは領域の幅(底辺の幅)を計算するためだけに用いている。これは各行の幅を定めるために必要な値だ。
(参照:末尾コード3)
ドアを描画する
Sastantuaの最下段にはドアがある。この部分についての仕様は定められていない。ピラミッド構成上の制約、サンプル図から次のことが分かる。
- ドアの高さ(行数)=指定された段数
- ドアの幅(文字数)=奇数
- ドアはピラミッドの中心に配置する。
ここでは次の仕様とした。
指定された段数が奇数の場合 | ドア幅=段数 |
指定された段数が偶数の場合 | ドア幅=段数-1 |
これを描画する処理を、最下段の描画処理に追加する。そのため描画処理はドアを含む行、含まない行で処理が分かれることになる。これまでの処理はドアを含まない行だ。最下段処理中、条件に合致する場合に、ドアを含む描画処理へ遷移する。
ドアを含む、含まない処理は、描画開始行、終了行を操作することで実現できないだろうか。描画中の行がドアの高さに達したら、開始行=終了行-指定段数(ドアの高さ)とする、ドアを含む描画処理に移行指せる。ドアを描画する行には次の要素が含まれる。
- 左側のスペース
- ピラミッド左側のブロック
- ドア
- ピラミッド右側のブロック
- 右側のスペース
ブロック、スペース幅(文字数)の調整は、ドア数を加味すればよい。これまでの処理と本質的に変わりはない。
余談
まだドアノブの処理が残っている。ドアノブの位置とサイズを仕様として定めたら、本質的には、これまでの処理と何も変わりがないことに気づいたと思う。段差ピラミッドのように、若干異なる処理を繰り返すのだ。もう、これ以上は採り上げない。
コードを見比べると分かるが、処理が重複していそうな箇所がある。関数を用いて構造化すれば分かりやすくなるのだが、Piscine流に自作関数も用いなかった。このような部分は、さらにロジックを効率化できる箇所なので、さらなる考えどころだ。
PiscineではC言語で実装することになる。私のサンプルの描画処理は、高さ(y方向)に繰り返すのみだが、Piscineでは横(行、x方向)についての繰り返し処理、つまりスペース、ブロック、ドアを生成する、各行の処理が必要になるだろう。ロジックを参考にするときは注意してほしい。
読者には知る由もないことだが、昨日から光回線が不通で、テザリング以外のインターネット接続する術がない。この課題はインターネット検索無しで仕上げた。分からないことはweb検索し、それでも分からなければ人に聞くのが、エコール42のスタンスだが、それらに頼らなくても、これくらいの課題には対応することができる。
By Odin, by Thor ! Use your brain !!!
サンプル・コード
コード1
ピラミッドの高さ(行数) | myarg |
描画領域の高さ | y_height |
描画領域の幅 | x_width |
描画中の行数 | y |
num_of_block | 該当行に含まれるブロック数 |
num_of_space | 該当行にすくまれるスペース数 |
myarg = 10 x_width = myarg + (myarg - 1) x_width += 2 y_height = myarg for y in range(y_height): num_of_block = 2 * (y + 1) - 1 num_of_space = int((x_width - num_of_block - 2) / 2) block = '/' + '*' * (num_of_block ) + '\\' left_space = right_space = ' ' * num_of_space print(y, '\t' + left_space + block + right_space)
コード2
ピラミッドの段数 | my_step |
Sastantuaの高さ | y_actual_height |
スキップ行数集計 | my_skip |
ピラミッドの高さ 描画領域の高さ |
y_offset_height |
描画領域の幅 | x_width |
描画中の行数 | y |
my_step = 3 y_actual_height = 0 my_skip = 0 for i_step in range(my_step): y_actual_height += i_step + 1 + 2 if i_step != 0: my_skip += int((i_step - 1) / 2) + 2 y_offset_height = y_actual_height + my_skip x_width = y_offset_height + (y_offset_height - 1) x_width += 2 for y in range(y_offset_height): num_of_block = 2 * (y + 1) - 1 num_of_space = int((x_width - num_of_block - 2) / 2) block = '/' + '*' * (num_of_block ) + '\\' left_space = right_space = ' ' * num_of_space print(y, '\t' + left_space + block + right_space)
コード3
ピラミッドの段数 | my_step |
Sastantuaの高さ | y_actual_height |
スキップ行数集計 | my_skip |
ピラミッドの高さ | y_offset_height |
描画領域の幅 | x_width |
line_from | 描画開始行 |
line_to | 描画終了行 |
描画中の行数 | y |
my_step = 5 y_actual_height = 0 my_skip = 0 for i_step in range(my_step): y_actual_height += i_step + 1 + 2 if i_step != 0: my_skip += int((i_step - 1) / 2) + 2 y_offset_height = y_actual_height + my_skip x_width = y_offset_height + (y_offset_height - 1) x_width += 2 for i_step in range(my_step): if i_step == 0: line_from = 0; line_to = i_step + 1 + 2 else: line_from = line_to + int((i_step - 1) / 2) + 2 line_to = line_from + i_step + 1 + 2 for y in range(line_from, line_to): num_of_block = 2 * (y + 1) - 1 num_of_space = int((x_width - num_of_block - 2) / 2) block = '/' + '*' * (num_of_block ) + '\\' left_space = right_space = ' ' * num_of_space print(y, '\t' + left_space + block + right_space)
コード4
ピラミッドの段数 ドアの高さ |
my_step |
Sastantuaの高さ | y_actual_height |
スキップ行数集計 | my_skip |
ピラミッドの高さ | y_offset_height |
描画領域の幅 | x_width |
line_from | 描画開始行 |
line_to | 描画終了行 |
描画中の行数 | y |
ドアの幅 | num_of_door |
my_step = 5 y_actual_height = 0 my_skip = 0 for i_step in range(my_step): y_actual_height += i_step + 1 + 2 if i_step != 0: my_skip += int((i_step - 1) / 2) + 2 y_offset_height = y_actual_height + my_skip x_width = y_offset_height + (y_offset_height - 1) x_width += 2 for i_step in range(my_step): if i_step == 0: line_from = 0; line_to = i_step + 1 + 2 else: line_from = line_to + int((i_step - 1) / 2) + 2 line_to = line_from + i_step + 1 + 2 if i_step != my_step - 1: for y in range(line_from, line_to): num_of_block = 2 * (y + 1) - 1 num_of_space = int((x_width - num_of_block - 2) / 2) block = '/' + '*' * (num_of_block ) + '\\' left_space = right_space = ' ' * num_of_space print(y, '\t' + left_space + block + right_space) else: for y in range(line_from, line_to - my_step): num_of_block = 2 * (y + 1) - 1 num_of_space = int((x_width - num_of_block - 2) / 2) block = '/' + '*' * (num_of_block ) + '\\' left_space = right_space = ' ' * num_of_space print(y, '\t' + left_space + block + right_space) for y in range(line_to - my_step, line_to): num_of_block = 2 * (y + 1) - 1 num_of_space = int((x_width - num_of_block - 2) / 2) if (my_step % 2) != 0: num_of_door = my_step else: num_of_door = my_step - 1 door = '|' * num_of_door left_block = '/' + '*' * int((num_of_block - num_of_door) / 2) right_block = '*' * int((num_of_block - num_of_door) / 2) + '\\' left_space = right_space = ' ' * num_of_space print(y, '\t' + left_space + left_block + door + right_block + right_space)