概要
「コンピュータで人生で初めて作った作品」を、
「人生で初めて触る言語」を使って再現する。
プログラミング言語はRustを使う。
...タイトル通り、Rustはほぼ触ったことがない。
厳密には、たったいまこの一文を書いているこの瞬間までのRustでの開発経験といえば「hello.rs」ぐらい(ソースコードの内容は...察せ)
つまりは、この計画は、初心のままに作ったプログラムを
「当時の初心ごと」再現するというものになってくる。素敵。
追記:初めて触る言語なのでコードは当然下手だと思われる...ので、なにか「ここもう少し賢い実装できるよ」とか「こういうのはしないほうがいいよ」などなどアドバイスがあれば是非是非お願いよろしくお願いします
どんな作品?
自分が中学二年生の頃にHTML+CSS+JSで作った、「マスあてゲーム」。
10x10のマスの並びの中から、答えとなる一つのマスを探し当てる。
どんなビジュアルだったのかははっきりと覚えているのだが、残念ながらソースを失ってしまったので復元ができない。というわけで、昔の記憶を思い出しながら再現していこうと思う。
進捗
1日目
コンパイラのインストール・hello.rsで動作をテスト。
Hello, world!
ここからPerukiiのRust人生が始まった....
プログラミング言語を学ぶなら書籍等を買うのがいいのかもしれないが、金欠なもので、しばらくは
The Rust Programming Language(日本語訳)
を参考に頑張ることにした。
上記サイトにて「数あてゲーム」が紹介されていたので、入門にいいかなと自分も作ってみることに。しかし(理解力0なので)よくわからず、途中で読むのを大挫折してしまった。というわけで、他のいろんなドキュメントも参考に、最終的にこんな感じの実装で完成させた。
extern crate rand;
use std::io;
use rand::Rng;
fn main() {
let target = rand::thread_rng().gen_range(1,10);
let mut val_st : String;
let mut val_ui : u32;
println!("数字を入力してください");
loop{
val_st = String::new();
io::stdin().read_line(&mut val_st).expect("入力処理に失敗しました");
val_ui = val_st.trim().parse().expect("数字ではないです。反則負けです");
if val_ui == target{
println!("当たったけどお前の負けです");
break;
}
else if val_ui < target{
println!("{} : よりもっと大きいです", val_ui);
}
else {
println!("{} : よりもっと小さいです", val_ui);
}
}
}
数字を入力してください
2
2 : よりもっと大きいです
8
8 : よりもっと小さいです
4
4 : よりもっと大きいです
7
7 : よりもっと小さいです
6
当たったけどお前の負けです
初めての自力実装。わいー
小並な感想
いい。
今まで触っていたCやC++と比べると、コードがすっきりしていてかなり可読性が高く&書きやすく見える。
2日目
入門してまだ2日目、文法もしっかりと把握できているわけではないもので相当早とちりなのかもしれないが...ここで早速ウインドウに描画できるライブラリを探索する。
いろいろ探ってみた結果、
クリエイティブコーディングライブラリnannouがいいかなと。
CやC++ではあんなに苦労していたライブラリのインストール、cargo.toml
に依存ライブラリの内容を書いてプロジェクトをビルドするだけで即インストール・使用ができた!すごい...
描画に挑戦
初めの一手はやはり...
use nannou::prelude::*;
fn main() {
nannou::sketch(view).run()
}
fn view(app: &App, frame: Frame) {
let draw = app.draw();
draw.background().color(WHITE);
draw.ellipse()
.color(Rgb::new(0.75,0.0,0.1))
.w(300.0)
.h(300.0);
draw.to_frame(app, &frame).unwrap();
}
描画にも慣れたところで、公式ガイドも見ながら
「マス当てゲーム」の土台となるマスをつくる。...た。
use nannou::prelude::*;
fn main() {
nannou::sketch(view).run()
}
fn view(app: &App, frame: Frame) {
let draw = app.draw();
draw.background().color(WHITE);
let scale = 50.0;
let trax = 400.0;
let tray = -300.0;
let mut locx:f32;
let mut locy:f32;
for x in 0..10{
for y in 0..10{
locx = scale*(x as f32)-trax;
locy = -scale*(y as f32)-tray;
draw.rect()
.color(Rgb::new(0.3, 0.3, 0.3))
.x(locx)
.y(locy)
.w(scale*1.05)
.h(scale*1.05);
draw.rect()
.color(Rgb::new(0.95, 0.95, 0.65))
.x(locx)
.y(locy)
.w(scale*0.95)
.h(scale*0.95);
}
}
draw.to_frame(app, &frame).unwrap();
}
これを実行すると
そうそう、まさにこんな感じである。
当時の作品のものはこんなシックな色ではなく、もっとガツガツに黄色や黒色だった気がするが...
目が痛くなりそうで開発がはかどらないので、最後にしておく。
粉感想
自分のPCのようなノーパソスペックだと、コンパイルが長い。
ちょっとイライラする感じ。まあこれは大体のモダン言語のコンパイラもそうなので仕方がないが。
あと、nannouを触っている間に改めて実感したのだが、Rustのコードの制約、かなり厳しい...
今までこういったことは耳にしかしてこなかったが、些細なことでもwarningやエラーを吐いてしまうので、厳しさがよくわかる (整数と浮動小数点数同士での演算をしようとしたら、値がどちらの型に吸収されるでもなくエラーを吐いたり....) 。
しかしこれがやはり、Rustの保守性の高さを示しているのか...
3日目
今日はたくさん時間があったので実装にふけた。
いろいろと学んだことがあった...
extern crate rand;
use rand::Rng;
use std::cmp;
use nannou::prelude::*;
struct Model{
table:[[bool; 10]; 10],
scale:f32,
trax:f32,
tray:f32,
targ:[i32; 2],
lstc:[i32; 2],
}
fn update(_app: &App, _model: &mut Model, _update: Update) {}
fn view(_app: &App, _model: &Model, _frame: Frame) {
let draw = _app.draw();
draw.background().color(WHITE);
let mut locx:f32;
let mut locy:f32;
// display table
for y in 0..10{
for x in 0..10{
locx = _model.scale*(x as f32)-_model.trax;
locy = -_model.scale*(y as f32)-_model.tray;
draw.rect()
.color(Rgb::new(0.3, 0.3, 0.3))
.x(locx)
.y(locy)
.w(_model.scale*1.05)
.h(_model.scale*1.05);
let color;
if _model.table[y][x] == true {
color = Rgb::new(0.3, 0.3, 0.3);
}
else if (_model.scale > abs(locx-_app.mouse.x)*2.0) && (_model.scale > abs(locy-_app.mouse.y)*2.0) {
color = Rgb::new(0.65, 0.65, 0.4);
}
else {
color = Rgb::new(0.95, 0.95, 0.65);
}
draw.rect()
.color(color)
.x(locx)
.y(locy)
.w(_model.scale*0.95)
.h(_model.scale*0.95);
}
}
// hint-text
let hintext:&str =
match cmp::max(abs(_model.targ[0]-_model.lstc[0]),abs(_model.targ[1]-_model.lstc[1]))
{
2 => { "Hint : NEAR the answer" }
1 => { "Hint : BY the answer!" }
0 => { "Hint : Congratulations! You hit the answer!" }
_ => { "Hint :" }
};
// display hint
draw.text(hintext)
.color(BLACK)
.font_size(40)
.x(-_model.trax*0.8)
.y(-_model.tray-_model.scale*10.5)
.no_line_wrap()
.left_justify();
// update
draw.to_frame(_app, &_frame).unwrap();
}
fn event(_app: &App, _model: &mut Model, _event: WindowEvent) {
match _event {
MousePressed(_button) => {
let unx = ((_app.mouse.x+_model.trax+_model.scale*0.5)/_model.scale) as i32;
let uny = -((_app.mouse.y+_model.tray+_model.scale*0.5)/_model.scale-1.0) as i32;
if unx < 10 && unx >= 0 && uny < 10 && uny >= 0 {
_model.table[uny as usize][unx as usize] = true;
}
_model.lstc = [unx, uny];
}
_ => {}
}
}
fn model(app: &App) -> Model {
let model_st = Model{
table:[[false; 10]; 10],
scale:50.0,
trax :400.0,
tray :-300.0,
targ :[rand::thread_rng().gen_range(0,9),rand::thread_rng().gen_range(0,9)],
lstc :[1000,1000]
};
app.new_window().event(event).view(view).build().unwrap();
return model_st;
}
fn main() {
nannou::app(model).update(update).run();
}
こんな感じになった
おお!まさにこんな感じ。
昔の思い出がどんどん蘇ってくるようで、開発がすごく楽しい。
...そしてここまでくるとそろそろ、今回作る作品の全貌が見えてきたところ。
ヒントに関しては、クリックしたマスと比較して、答えのマスが
「縦5マス、横5マスの範囲内」だと"近い!(NEAR)"
「縦3マス、横3マスの範囲内」だと"すごく近い!!(BY)"
クリックしたまさにそのマスだと"正解!(Congratulations!)" となる。
粉感
今日の開発を通じて驚かされたのは、値の変更可能(mutable)なグローバル変数を宣言できないということ。
グローバル変数はあんまり良くないなんてことはよく聞いていたが、Rustではとうとう禁止されてしまったのか...と腰を抜かしてしまった。
そのため複数の関数同士で変数の値を共有するには、参照渡しでなんとか頑張る必要がある。ズボラで過去にはグローバル変数を使いまくっていた自分にとっては結構慣れない実装で大変苦労した。けどその分なにかとてもいい教育にもなった気がする。
もう一つ大きな発見として、Rustの簡単な文法(変数の宣言、if文など)ならレファレンスを見なくても自力で書けるようになっていた。Rustがどんどん自分の中で親しいものに感じてくる...
おもしれー言語
思い出すのは... (雑談)
こういう開発をしていると、いろいろ思い出すものがある。
GetElementById
なんて関数があったような、なかったような...
JSもHTMLもう長らく触っていないから、その機能は全く覚えていないが。
当時は中学生でElement
By
とかの英語の言葉の意味を知らなかった上、そもそもこの関数名が英語から来ていることにも気づかず**"単なる謎の文字列"**だと考えていたもので、関数名のスペルを最後まで覚えることができなかったガキであった...
そんなこともあり、この関数を打つときは毎回のように技術書を開き直しては、人差し指で写経をするようにタイプをしていた。ああ懐かし。
あの頃に比べると、今の自分はずいぶん成長してるなあ、など。雑談終わり。
4日目:完成
試行錯誤の結果、最終的にこんな感じで完成した。
extern crate rand;
use rand::Rng;
use std::cmp;
use nannou::prelude::*;
struct Model{
table:[[bool; 10]; 10],
scale:f32,
trax:f32,
tray:f32,
targ:[i32; 2],
lstc:[i32; 2],
demon:[[i32; 2]; 3],
count:i32,
game:bool,
demf:bool,
}
fn update(_app: &App, _model: &mut Model, _update: Update) {}
fn view(_app: &App, _model: &Model, _frame: Frame) {
let draw = _app.draw();
draw.background().color(WHITE);
let mut locx:f32;
let mut locy:f32;
// display table
for y in 0..10{
for x in 0..10{
locx = _model.scale*(x as f32)-_model.trax;
locy = -_model.scale*(y as f32)-_model.tray;
draw.rect()
.color(Rgb::new(0.3, 0.3, 0.3))
.x(locx)
.y(locy)
.w(_model.scale*1.05)
.h(_model.scale*1.05);
let color;
if _model.table[y][x] == true {
let mut demonf:bool = false;
for i in 0..3{
if x as i32 == _model.demon[i][0] && y as i32 == _model.demon[i][1] {
demonf = true;
}
}
if demonf == true {
color = Rgb::new(0.3, 0.3, 1.0);
}
else if x as i32 == _model.targ[0] && y as i32 == _model.targ[1] {
color = Rgb::new(1.0, 0.3, 0.3);
}
else {
color = Rgb::new(0.3, 0.3, 0.3);
}
}
else if (_model.scale > abs(locx-_app.mouse.x)*2.0) && (_model.scale > abs(locy-_app.mouse.y)*2.0) {
color = Rgb::new(0.65, 0.65, 0.4);
}
else {
color = Rgb::new(0.95, 0.95, 0.65);
}
draw.rect()
.color(color)
.x(locx)
.y(locy)
.w(_model.scale*0.95)
.h(_model.scale*0.95);
}
}
// hint-text
let hintext:&str =
match _model.demf{
true => { "DEMON : Game Over" }
false => { match cmp::max(abs(_model.targ[0]-_model.lstc[0]),abs(_model.targ[1]-_model.lstc[1]))
{
2 => { "Hint : NEAR the answer" }
1 => { "Hint : BY the answer!" }
0 => { "Congratulations! You hit the answer!" }
_ => { "Hint :" }
}
}
};
// display hint
draw.text(hintext)
.color(BLACK)
.font_size(40)
.x(-_model.trax*0.8)
.y(-_model.tray-_model.scale*10.5)
.no_line_wrap()
.left_justify();
// display count
draw.text(&("Count : ".to_string() + &_model.count.to_string()))
.color(BLACK)
.font_size(40)
.x(-_model.trax*0.8)
.y(-_model.tray-_model.scale*11.5)
.no_line_wrap()
.left_justify();
// update
draw.to_frame(_app, &_frame).unwrap();
}
fn event(_app: &App, _model: &mut Model, _event: WindowEvent) {
match _event {
MousePressed(_button) => {
if (_model.game || _model.demf) == false {
let unx = ((_app.mouse.x+_model.trax+_model.scale*0.5)/_model.scale) as i32;
let uny = -((_app.mouse.y+_model.tray+_model.scale*0.5)/_model.scale-1.0) as i32;
if unx < 10 && unx >= 0 && uny < 10 && uny >= 0
&& _model.table[uny as usize][unx as usize] == false {
_model.table[uny as usize][unx as usize] = true;
_model.count += 1;
_model.lstc = [unx, uny];
if unx==_model.targ[0] && uny==_model.targ[1] {
_model.game = true;
}
else {
for i in 0..3{
if unx==_model.demon[i][0] && uny==_model.demon[i][1] {
_model.demf = true;
}
}
}
}
}
}
_ => {}
}
}
fn rndp() -> [i32; 2] {
return [rand::thread_rng().gen_range(0,9), rand::thread_rng().gen_range(0,9)];
}
fn model(app: &App) -> Model {
let model_st = Model{
table:[[false; 10]; 10],
scale:50.0,
trax :400.0,
tray :-300.0,
targ :rndp(),
lstc :[1000,1000],
demon:[rndp(),rndp(),rndp()],
count:0,
game :false,
demf :false,
};
app.new_window().event(event).view(view).build().unwrap();
return model_st;
}
fn main() {
nannou::app(model).update(update).run();
}
このゲームの全貌
ルールはこんな感じになる。
- 10x10マスの中からヒントを参考にしながら当たりを探す
- 見つけたらゲームクリア
- 3つだけある「DEMON」のマスを選択してしまうとGame Over
惜しいのはやはり、nannouのtextが日本語(Unicode)に対応していなかったことである(頑張ればできるんだろうが...)
日本語に対応していれば、ヒントメッセージは
「正解!」
「悪魔が来た GAME OVER」
といった、もっと中学生らしい無垢な表現を再現できた。
最後に、色も当時のまま完璧に再現すると、下のようになった。
ああ...
開発を通じて学んだこと
簡単なプログラムのはずなのだが、すごくいろんなことを学べてよかった。
中学生の自分でも作れたような簡単なプログラムで、複雑な文を書かずともすぐに再現できた。そのため新しく学ぶ言語での実装も苦労しなかった。
そして、Rustというもの、自分が思っているよりもはるかに奥深いんだろうと思う
( C++の奥深さを知らず一度痛い目にあったことがあったもんで、Rustにもありそう.... )
ので、今後経験を積んでぜひとも使いこなしていきたいところ。こういうの使いこなせたら...かっこいいじゃん??
番外編
5日目:fmtとclippyの利用
@maguro_tuna 氏にコメントにて、「rustfmtやclippyを使うとよりRustらしいコードになりますよ」というありがたいお言葉をいただいたので、早速試してみた。
...なんやねんそれ
rustfmtを使ったところ、書いたコードをより見やすいように加工したものを自動で再記述&上書きしてくれた。
一方clippyは、書いたコードの不適切(結構厳しい)な実装を注意してくれるソフトウェアみたい。
(ちなみにこういうの、cpplint
にGoogle C++ Style Guideがあるように、このrustfmtにも何かしらのコードのスタイルの規定があったりするのだろうか...?)
どちらも使ってみた感じだが、
$ cargo fmt
$ cargo clippy
とするだけで一発でやってくれた(rustupを導入した場合)。すごい。めっちゃ簡単。
そして、やってみたところ
warning: unneeded `return` statement
--> src/main.rs:149:5
|
149 | / return [
150 | | rand::thread_rng().gen_range(0, 9),
151 | | rand::thread_rng().gen_range(0, 9),
152 | | ];
| |______^
|
= note: `#[warn(clippy::needless_return)]` on by default
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_return
help: remove `return`
|
149 | [
150 | rand::thread_rng().gen_range(0, 9),
151 | rand::thread_rng().gen_range(0, 9),
152 | ]
|
.
.
.
~ NANYA KANYA ~
.
.
.
warning: equality checks against false can be replaced by a negation
--> src/main.rs:126:24
|
126 | && _model.table[uny as usize][unx as usize] == false
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try simplifying it as shown: `!_model.table[uny as usize][unx as usize]`
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#bool_comparison
warning: 8 warnings emitted
Finished dev [unoptimized + debuginfo] target(s) in 50.46s
...と、さっきのコード(にrustfmtをかけたやつ)だけで8つものwarningを手にしてしまった。
warningを一通り読んでみると、主に
- if文でいいようなところをわざわざmatchで書いててきもちわる〜
- return、いらんが....
と言った感じであった(他にもいろいろあったがダイジェスト)。
粉う
clippyの吐いたwarningをもとにしたコードの訂正を通じて、Rustの文法の特徴をより深く学ぶことができたのは大きい。returnいらないなんて知らなかったし、あとセミコロンの使い時、if文の賢い使い方なんかも改めて理解できた。
あと、単純にこのようなlintツールをこんなに手軽に使えることにも感動した。OSSへのコントリビューションとかもバリバリ捗りそう。
そして全体的に、Rustのライブラリやツールの導入のしやすさになんとも驚かされた。
ライブラリの場合だと、CやC++ではgit clone
からの./configure``make
やら、あるいはmeson``ninja
とか...いろいろ使わないとインストールできなかったものが、Rustではコマンドさえなくとも随時*.toml
に記述すればビルド時に勝手にしてくれたり。
...まあ、こういうのはモダンなコンパイラじゃ既に当然なのかもしれないが...
とにかく、改めて--- @maguro_tuna 先生、アドバイスいただきありがとうございました。
おわりに
初めての言語で「人生初の作品」を再現するというのは、本記事に限らずみなさんにとっても結構面白い経験になりそう。というわけでみんなもやってみよう。
## 完