🙌

今日の個人開発4~Rustアプリの機能改善・追加~

2023/02/19に公開

はじめに

この記事は、coloname33が不定期で行う小規模個人開発の記録です。

本日のテーマ

前回の続きです。

機能改善・追加をしてみます。

環境

Rust

% cargo --version
cargo 1.67.1 (8ecd4f20a 2023-01-10)

作るもの

  • スコア表示
  • CodeをHashMapに変更
  • 桁数を変えられるようにする

実装

スコア表示

counterを定義して、loopの中でインクリメントし、最後に表示する実装をしました。

https://github.com/kyuki3rain/hit-and-blow-rust/commit/8e31283b3cc00d53cca1ae67a9e301698324a161

CodeをHashMapに変更

HashMapへの変更

まず、数字の重複の検出を簡単にするため、codeの中身をVecから数字をkey、位置のインデックスをvalueとするHashMapに変更しました。

- pub struct Code(pub Vec<u8>);
+ pub struct Code(pub HashMap<u8, usize>);

これにより、初期化処理は以下のようになります。

HashMapは[(key, value), (key, value), ...]のiterから生成できます。

    pub fn from_rand() -> Result<Self, String> {
        let mut rng = rand::thread_rng();
        let choices: Vec<u8> = (0..10).collect();

        Ok(Code(HashMap::from_iter(
            choices
                .choose_multiple(&mut rng, 4)
                .cloned()
                .enumerate()
                .map(|(i, d)| (d, i)),
        )))
    }

この変更により、from_string内の重複判定やhit, blowの判定では、HashMapのinsertやgetを使うことで高速化できます。

if let Some(j) = code.insert(d, i) {
    return Err(format!(
        "{}つ目と{}つ目の数字が重複しています。d={}",
        j + 1,
        i + 1,
        d
    ));
}
if let Some(j) = answer.0.get(val) {
    if i == j {
        hit += 1;
    } else {
        blow += 1;
    }
}

桁数を変えられるようにする

テストの追加

まずは桁数が増えた場合のテストを加えます。

from_stringは最初から桁数依存ではなかったため省きます。(10桁より大きい値は重複チェックにより弾かれます。)

let answer = Code::from_string("01234567".to_string()).unwrap();
let guess = Code::from_string("01234567".to_string()).unwrap();
assert_eq!(
    CheckResult::check(&answer, &guess),
    Ok(CheckResult { hit: 8, blow: 0 })
);
let result = CheckResult { hit: 8, blow: 0 };
assert!(result.correct(8));
let code = Code::from_rand(8).unwrap();

assert_eq!(code.0.len(), 8);

let mut set = HashSet::new();

for (d, i) in &code.0 {
    assert!(*d < 10);
    assert!(set.insert(*i));
}

当然これらのテストは今は失敗しますが、ここからの改変によって通るようにしていきます。

桁数の変数化

今まで定数の4としていた桁数を変数lenに置き換えます。

from_randでは、桁数が10以下でないと重複が発生してしまうため、エラーを新たに追加しました。

pub fn from_rand(len: usize) -> Result<Self, String> {
    if len > 10 {
        return Err(format!("長さは10以下である必要があります。l={}", len));
    }
    let mut rng = rand::thread_rng();
    let choices: Vec<u8> = (0..10).collect();

    Ok(Code(HashMap::from_iter(
        choices
            .choose_multiple(&mut rng, len)
            .cloned()
            .enumerate()
            .map(|(i, d)| (d, i)),
    )))
}

CheckResultの方では、check関数に変化はなく、correct関数が新たにlenを引数にとって使用するようになります。

関数が小さく変数を持たせたくない気持ちがあり、CheckResult型に持たせるのを検討しましたが、判定のたびにインスタンスが生成されるのも気になったので、今回は引数にしました。

pub fn correct(&self, len: usize) -> bool {
    self.hit == len && self.blow == 0
}

clapの導入

ここまでで、lenを変数にすることができたので、あとはこれをコマンドの引数として外から指定できるようにします。

clapを導入しました。

clap = { version = "4.1.6", features = ["derive"] }

これにより、簡単にコマンド引数を実装することができます。

以下のようにArgsを定義し、lenの代わりにargs.lengthを渡すように変更しました。

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// Number of length of code
    #[arg(short, long, default_value_t = 4)]
    length: usize,
}

fn main() {
    let args = Args::parse();

    let answer = match Code::from_rand(args.length) {
        Ok(code) => code,
        Err(e) => {
            println!("{}", e);
            return;
        }
    };
}

実際のコードが以下になります。

https://github.com/kyuki3rain/hit-and-blow-rust/tree/release/da842f5bc4736a

最後に

今回はAPIゲームの改良をやっていきました。

続きはこちら:
今日の個人開発5~Factoryパターンに基づくCodeの改良~

記事については、もっと読みやすくなるような修正を適宜していくつもりです。

実装こっちの方がいいよ!などあればコメントください。

Discussion