今日の個人開発6~答えの選択肢を表示する~
はじめに
この記事は、coloname33が不定期で行う小規模個人開発の記録です。
本日のテーマ
前回の続きです。
機能改善・追加をしてみます。
環境
Rust
% cargo --version
cargo 1.67.1 (8ecd4f20a 2023-01-10)
作るもの
- Log
- Possibility
Possibilityについて
hit&blowにおいて、答えになりうる選択肢は質問の度に減っていきます。
この選択肢の一覧を管理するのがPossibilityになります。
Possibilityには、まずcodeがとりうる値をすべて保持しておき、これを各回の結果から絞り込んでいくことで絞り込んでいきます。
実装
Logの定義
以下のように、models/log.rsにguessとresultをもつLogという構造体を定義し、main関数で生成します。
Logの表示は、そのままresultを表示するように設定します。
use std::fmt;
use super::{Code, DiffResult};
#[derive(Debug)]
pub struct Log {
pub guess: Code,
pub result: DiffResult,
}
impl fmt::Display for Log {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.result)
}
}
let log = Log { guess, result };
Possibilityの定義
まず、models/possibility.rsにVec<Code>
のnewtypeとしてPossibilityを定義します。
PossibilityはVec<Code>
から返還できるようにfromトレイトを実装しました。
use std::fmt;
use super::{Code, Log};
pub struct Possibility(Vec<Code>);
impl From<Vec<Code>> for Possibility {
fn from(value: Vec<Code>) -> Self {
Self(value)
}
}
Possibilityの表示は、以下のように選択肢の数と、値を最大5つ表示するようにします。
impl fmt::Display for Possibility {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = format!("counts: {}\n", self.0.len());
s += "possibilities: ";
for code in self.0.get(0..5).unwrap_or(&self.0) {
s += &format!("{}, ", code);
}
if self.0.len() > 5 {
s += " etc...";
}
write!(f, "{}", s)
}
}
argumentの定義
Argsにpossibilityを定義して、可能性の列挙を行うかどうかを制御します。
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
...
/// calc possibility: [true, false]
#[arg(short, long, default_value_t = false)]
possibility: bool,
}
Possibilityの初期化
全列挙関数の実装(ChatGPT)
Possibilityの初期化にあたって、可能性のある数字列を全列挙する必要があります。
正確に言えば、「0~(radix-1)までの数字をlen桁並べるときのパターンを全列挙するプログラム」が必要です。
せっかくなので、ChatGPTに作成をお願いしてみたところ、以下のプログラムができたので、libs/perm.rs
としてそのまま利用しました。
pub fn generate_permutations(radix: u32, len: usize) -> Vec<Vec<u8>> {
let mut results = Vec::new();
let mut permulation = vec![0; len];
generate_permutations_helper(&mut permulation, &mut results, 0, radix);
results
}
pub fn generate_permutations_helper(
permulation: &mut [u8],
results: &mut Vec<Vec<u8>>,
index: usize,
radix: u32,
) {
if index == permulation.len() {
results.push(permulation.to_vec());
return;
}
for i in 0..(radix as u8) {
permulation[index] = i;
generate_permutations_helper(permulation, results, index + 1, radix);
}
}
まさかの一発動作で感動しました。
generate_allの実装
次に、code_factoryにgenerate_allを実装します。
先ほどのプログラムではVec<Vec<u8>>
となっているのですが、せっかくなのでこのまま使うために、出力結果を再びループしてCode
に整形し直す処理を加えます。無駄があるので、のちのちリファクタリングするかもです。
// 返還は2度手間だけど、せっかくChatGPTちゃんが作ってくれたlibなのでそのまま使います
pub fn generate_all(&self, len: usize) -> Vec<Code> {
generate_permutations(self.to_radix(), len)
.iter()
.map(|vec| Code::new(vec.iter().enumerate().map(|(i, d)| (*d, i)).collect()))
.collect()
}
また、この処理の実装場所については、実質Possibilityのコンストラクタでもあるので迷いましたが、情報の分散を嫌ってこのままいくことにしました。
今後、Modeを表すEnumを引き渡す形に統一するかもです。再検討します。
main.rsでの使用
最後に、main.rs
でargsを参照し、必要であれば初期化をおこなう処理を追加しました。
possibilityの計算で一番重たいのはこの全列挙の処理なので、最初の時点で分けてしまう実装としています。
let mut possibility: Possibility = if args.possibility {
factory.generate_all(args.length).into()
} else {
Possibility::new()
};
possibilityの更新と表示
最後に、guessの1ループが終わった後、possibilityの更新を行う機能を追加します。
この実装は、前のvecから条件を満たす場合だけ残して他は棄てるフィルターのような機能が実現できる、retainというメソッドを使用しています。
impl Possibility {
pub fn update(&mut self, log: &Log) {
self.0.retain(|code| {
if let Ok((result, _)) = code.diff(&log.guess) {
result == log.result
} else {
false
}
});
}
}
そして、これをmain関数で使用し、都度その結果を表示するようにします。
if args.possibility {
possibility.update(&log);
println!("{}", possibility);
}
おまけ
guessの入力・生成・比較を一つのメソッドとして束ね、logを返すように変更しました。
これもlogのコンストラクタとも言えますが、処理が重たいので別で分けたくなりました。
use std::io;
use crate::factories::CodeFactory;
use crate::models::{Code, Log};
pub fn guess(factory: &CodeFactory, answer: &Code) -> Result<(Log, bool), String> {
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("入力エラー。read_line()で失敗しました。");
let guess = factory.generate_from_str(&guess)?;
let (result, is_correct) = answer.diff(&guess)?;
Ok((Log { guess, result }, is_correct))
}
今後、普通のメソッドはfeaturesというフォルダに区切って造ろうと思います。
ちなみに、このようにメソッドでくるんだおかげで、エラーハンドリングを共通化できました。
使用側はこんな感じになります。
let (log, is_correct) = match guess(&factory, &answer) {
Ok(r) => r,
Err(e) => {
println!("{}\nもう一度入力してください。", e);
continue;
}
};
実装したコード
実際のコードが以下になります。
https://github.com/kyuki3rain/hit-and-blow-rust/tree/release/da842f5bc4736a
最後に
今回は選択肢を表示するアルゴリズムを実装しました。
続きは多分あります。
記事については、もっと読みやすくなるような修正を適宜していくつもりです。
実装こっちの方がいいよ!などあればコメントください。
Discussion