🙌

今日の個人開発6~答えの選択肢を表示する~

2023/03/04に公開

はじめに

この記事は、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を表示するように設定します。

models/log.rs
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)
    }
}
main.rs
let log = Log { guess, result };

Possibilityの定義

まず、models/possibility.rsにVec<Code>のnewtypeとしてPossibilityを定義します。

PossibilityはVec<Code>から返還できるようにfromトレイトを実装しました。

models/possibility.rs
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つ表示するようにします。

models/possibility.rs
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を定義して、可能性の列挙を行うかどうかを制御します。

main.rs
#[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としてそのまま利用しました。

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に整形し直す処理を加えます。無駄があるので、のちのちリファクタリングするかもです。

code_factory.rs
    // 返還は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の計算で一番重たいのはこの全列挙の処理なので、最初の時点で分けてしまう実装としています。

main.rs
    let mut possibility: Possibility = if args.possibility {
        factory.generate_all(args.length).into()
    } else {
        Possibility::new()
    };

possibilityの更新と表示

最後に、guessの1ループが終わった後、possibilityの更新を行う機能を追加します。

この実装は、前のvecから条件を満たす場合だけ残して他は棄てるフィルターのような機能が実現できる、retainというメソッドを使用しています。

models/possilibity.rs
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関数で使用し、都度その結果を表示するようにします。

main.rs
if args.possibility {
    possibility.update(&log);
    println!("{}", possibility);
}

おまけ

guessの入力・生成・比較を一つのメソッドとして束ね、logを返すように変更しました。

これもlogのコンストラクタとも言えますが、処理が重たいので別で分けたくなりました。

features/guess.rs
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というフォルダに区切って造ろうと思います。

ちなみに、このようにメソッドでくるんだおかげで、エラーハンドリングを共通化できました。

使用側はこんな感じになります。

main.rs
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