ポーカーを作りたい【未完成】

前回の記事ではCardクラスを作った。

amvirosa.hateblo.jp

なおポーカーには飽きた。プログラミングによりハマっている。

Cardクラス

class Card{
    const int id_;    
    const int suit_;
    const int value_;

    public:

    Card():Card(-1,-1,-1){}
    Card(const int id,const int suit,const int num)
    :id_(id),suit_(suit),value_(num){}
    Card(const Card &cd)
    :id_(cd.id_),suit_(cd.suit_),value_(cd.value_){}
    Card(Card&& m) noexcept
    :id_(std::move(m.id_)),suit_(std::move(m.suit_)),value_(std::move(m.value_)){}
    ~Card()=default; 

    void PrintCard()const{
        std::cout << SuitEmoji() << Value();
    }
    std::pair<std::string,std::string> Mark()const;
    std::string SuitEmoji()const;
    std::string Value()const;    
};
std::string Card::Value() const{
    if (value_ > 1 && value_ < 10){
        std::string result = std::to_string(value_);
        return result;
    }
    switch (value_){
    case 1:     return "A";
    case 10:    return "T";
    case 11:    return "J";
    case 12:    return "Q";
    case 13:    return "K";
    default:    return " ";
    }
}
std::string Card::SuitEmoji() const{
    switch (suit_){
    case 1:     return "♠";
    case 2:     return "♣";
    case 3:     return "♥";
    case 4:     return "♦";
    default:    return " ";
    }
}
std::pair<std::string, std::string> Card::Mark() const{
    return {SuitEmoji(), Value()};
}

記号はcharではだめで、考えるのがめんどくさくstd::stringで一文字を返している。

律儀にムーブコンストラクタまで実装しているが、理解できていないので以降のクラスでは実装できていない。ばか。

切り詰めればメンバ変数はカードIDのみで諸々の処理を行えそうだが、そこまで切羽詰まった環境でプログラミングしていないので贅沢にメンバを宣言する。

Deckクラス

Cardクラスの配列を持つ。

class Deck{
    std::vector<Card> deck;
    std::vector<int> list;

public:
    Deck(){
        int count = 1;
        for (int i = 1; i < 5; ++i){
            for (int j = 1; j < 14; ++j){
                int id = count;
                int suit = i;
                int value = j;
                deck.emplace_back(id, suit, value);
                list.emplace_back(id);
                count ++;
            }
        }
    }
    std::vector<int>& shuffle(std::mt19937& engine){
        std::shuffle(list.begin(),list.end(),engine);
        return list;
    }
    void Ref(const int id){
        const int index=id-1;
        deck.at(index).PrintCard();
    }
};

デッキシャッフルを要求するとカードID配列をシャッフルしたものの参照を返している。

カードIDはdeck.at(カードID-1)と等しい。なぜこんな面倒なことになっているのかというとカードIDは1からカウントしているため。素直に0始まりにすればいいのに……(やっぱり直そう……)悲しみを背負うRef関数はCardクラスのPrintCard()関数を呼ぶ。

Playerクラス

class Player{
    int id_;
    std::array<std::optional<int>,5> hands_;

    size_t cardcount_;

    public:
    Player():Player(-1){}
    Player(const int id)
        :id_(id),
        hands_{std::nullopt,std::nullopt,std::nullopt,std::nullopt,std::nullopt},
        cardcount_(0){}    
    ~Player()=default;

    void AddCard(const int card_id){
        hands_.at(cardcount_)=card_id;
        cardcount_++;
    }
    bool fullOfHands(){
        if(cardcount_==5)return true;
        return false;
    }
    std::array<std::optional<int>,5>& OpenHands(){
        return hands_;
    }
    void hands_size()const{
        std::cout << cardcount_;
    }
    void Init(){
        for(auto& item:hands_){
            item=std::nullopt;
        }
        cardcount_=0;
    }
};

手札の情報をstd::optionalで管理している。最大5枚。

実際に手の内にあるのはカードidだけ。unique_ptrとかshared_ptrを保持するほうが良いのだろうか。

カード枚数を数えるcardcount_があるのはなんだか余分な気がする。普通にvector<optional>で良いはず。std::arrayを見捨てられなかった。

Pokerクラス

enum class State{
    Opening,
    PreFrop,
    PostFrop,
    Turn,
    River,
    ShowDown,
    Ending,
};
class Poker{
    State st;
    std::string userinput;
    bool is_continue;
    std::unique_ptr<Deck> deck;
    std::vector<int> list;
    Player BOARD;
    std::mt19937& mt;
    std::array<Player,6> players;

    public:
    Poker(std::mt19937& mt)
    :st(State::Opening),userinput(""),is_continue(true),
     deck(std::make_unique<Deck>()),
     BOARD(Player(-1)),
     mt(mt){
        std::cout <<"テキサスホールデムへようこそ\n";
    }
    bool YesOrNo(const std::string& st){
        if(st =="Y"||st=="y"||st=="Yes"||st=="YES"||st=="yes")return true;
        return false;
    }
    void Update(){
        switch(st){
        case State::Opening   : Opening()   ; return;
        case State::PreFrop   : PreFrop()   ; return;
        case State::PostFrop  : PostFrop()  ; return;
        case State::Turn      : Turn()      ; return;
        case State::River     : River()     ; return;
        case State::ShowDown  : ShowDown()  ; return;
        case State::Ending    : Ending()    ; return;        
        }
    }
    bool isContinue()const{
        return is_continue;
    }

    private:
    void Opening(){
        std::cout <<"ゲームを開始しますか?[y/n]:";
        std::cin >> userinput;
        if(YesOrNo(userinput)){
            st =State::PreFrop;
            return;
        }
        st =State::Ending;
        return;
    }

    void PreFrop(){
        std::cout <<"プリフロップ:";
        InitPlayers();
        list =deck->shuffle(mt);
        SendCardForPlayers();
        SendCardForPlayers();

        const auto user =players.at(0).OpenHands();
        std::cout <<"あなたの手札は";
        PrintCardofId(user);

        std::cout <<"\n 続けますか?[y/n]";
        std::cin >>userinput;

        YesOrNo(userinput) ? st=State::PostFrop : st=State::Opening ;

        return;
    }

    void PostFrop(){
        std::cout <<"フロップ:\n";
        MakingBOARD(3);
        std::cout <<"ボードは";
        const auto board =BOARD.OpenHands();
        PrintCardofId(board);
        
        const auto user =players.at(0).OpenHands();
        std::cout <<"\nあなたの手札は";
        PrintCardofId(user);
        
        std::cout <<"\n 続けますか?[y/n]";
        std::cin >>userinput;
        YesOrNo(userinput) ? st=State::Turn : st=State::Opening ;

        return;
    }

    void Turn(){
        std::cout <<"ターン:";
        std::cout <<"ボードは";

        SendCardForBOARD();
        const auto board =BOARD.OpenHands();
        PrintCardofId(board);
        
        const auto user =players.at(0).OpenHands();
        std::cout <<"\nあなたの手札は";
        PrintCardofId(user);
        
        std::cout <<"\n 続けますか?[y/n]";
        std::cin >>userinput;
        YesOrNo(userinput) ? st=State::River : st=State::Opening ;
        return;
    }

    void River(){
        std::cout <<"リバー:";

        std::cout <<"ボードは";
        SendCardForBOARD();
        const auto board =BOARD.OpenHands();
        PrintCardofId(board);
        
        const auto user =players.at(0).OpenHands();
        std::cout <<"\nあなたの手札は";
        PrintCardofId(user);
        
        std::cout <<"\n 続けますか?[y/n]";
        std::cin >>userinput;
        YesOrNo(userinput) ? st=State::ShowDown : st=State::Opening ;
        st=State::ShowDown;
        return;
    }

    void ShowDown(){
        std::cout <<"ショーダウン:";
        std::cout <<"ボードは";
        const auto board =BOARD.OpenHands();
        PrintCardofId(board);

        const auto user =players.at(0).OpenHands();
        std::cout <<"\nあなたの手札は";
        PrintCardofId(user);

        std::cout <<"\n";

        ShowDownPlayers();

        st=State::Ending;
        return;
    }

    void Ending(){
        InitPlayers();
        std::cout <<"ゲームを終了しますか?[y/n]:";
 
        std::cin >> userinput;
        if(YesOrNo(userinput)){
            is_continue=false;
            return;
        }
        st =State::Opening;
        return;        
    }


    void InitPlayers(){
    for(int count =0;auto &player:players){
        player = Player(count);
        count++;
    }
    BOARD=Player(-1);
    }

    void PrintCardofId(const std::array<std::optional<int>,5>& hand){
        for(const auto& card_id:hand){
            if(card_id.has_value()){

                int id =card_id.value();
                deck->Ref(id);
            }
        }

    }
    void ShowDownPlayers(){
        for(auto i=1;i<players.size();i++){
            std::cout <<"NPC" <<i <<":";
            const auto npc =players.at(i).OpenHands();
            PrintCardofId(npc);

            std::cout <<"\n";
        }

    }
    void SendCardForPlayers(){
        for(auto count =0;auto player:players){
            players.at(count).AddCard(list.back());

            list.pop_back();

            count++;
            
        }
    }
    void MakingBOARD(const int count){
        for(auto i=0;i<count;i++){
        SendCardForBOARD();
        }
    }
    void SendCardForBOARD(){
        BOARD.AddCard(list.back());
        list.pop_back();
    }

};

クソデブクラスである。

Playerlistを持っているがそれとは別にPlayer型のBOARD(なぜ大文字……)をもつ。今思えばユーザーも特別扱いしてNPCのみをplayersで管理するべきかも。

内部状態を列挙型Stateの値で保持し、Update関数は迫真のswitch文で分岐する。継承が泣いている。

役判定が未実装。現状カードを引くだけのゲームであり、NPCはフォールドを知らない。こんなのポーカーじゃないよ

main関数

int main(){
    std::random_device seed;
    std::mt19937 mt(seed());
    Poker poker(mt);
    while(poker.isContinue()){
        poker.Update();
    }
    std::cout <<"お疲れさまでした\n";
}

乱数生成器をpokerに渡してゲーム開始。 isContinue()がfalseを返したらゲーム終了。

乱数生成器はデッキクラスに持っていてほしいのだが、なんだかうまくいかなかった(こどもみたいなかんそう)

TODO

役判定システム、行動順、レイズorコール、NPCの行動。

ゲーム作るのってむずかしいんだなぁ……

素人がWSL Ubuntu18.04LTSを日本語化しようとしてミスしたらハメ技を食らった

Ubuntuわからない

そうだ日本語化しよう

英語が苦手なので日本語化します

1.とりあえず最新に保つ儀式

sudo apt update -パッケージ一覧を更新

sudo apt upgrade -実際にパッケージを更新

なにかを追加する前にとりあえず唱えておく。

更新しなくていい場合はなにもしなくていいとUbuntu君は判断してくれるので、唱え得。

2.日本語化パッケージの取得

sudo apt install language-pack-ja -日本語化パッケージをインストール

そのままだ。つまりUbuntuではsudo apt install xxxのxxxにほしいパッケージ名を書けばインストールされるということ(そのままだ)

sudo apt -y install xxx と-yを入れると途中のyes/no選択肢を全部yesにして自動でインストールできる。

どのようなパッケージが用意されているのかは公式サイトを見れば良さそう packages.ubuntu.com

Ubuntu18.04LTSはbionicというお名前があり、そこを見ると18.04がインストールできるものがたくさん並んでいる。

3.日本語に切り替える

sudo update-locale LANG=ja_JP.UTF8 日本語(ja_JP.UTF8)にする

当然だがインストールした時点ではインストールしただけであるので、日本語にセットする必要がある。

Ctrl+Dで一度終了し、再度起動すれば日本語になっている、はず。

locale で表示されるものが大体ja_JP.UTF8ならば成功。

成功!

しかし、この成功をつかみ取る道中でタイトルの通りのハメ技を食らった。

/etc/default/localeに不正な値が入って詰みかけた

$ cat /etc/default/locale
LANG=ja_JP.UTF8
=ja_UTF8 // おそらくミスタイプが原因で挿入された不正値

日本語設定の方法を調べながら色々試していたら、/etc/default/localeの中に不正値が入って大変なことになった。

上記は状況を再現したもの。

この状態ではsudoコマンドが使えなくなる

具体的にはこのようなエラーが出る。

$ sudo xx// root権限が必要な操作を入力
sudo: pam_open_session: Bad item passed to pam_*_item()
sudo: policy plugin failed session initialization

$ 

この不具合報告は日本語情報がほとんどなく、同様の報告があるのはこのページしか見つからなかった。

sudoはパスワードを用いた認証でroot権限を与えるみたいな仕組みをしており、pamなんとかがパスワード認証を司っている。

そしてpamは処理の過程で/etc/default/localeを通るので、/etc/default/localeに不正値が入っていると変なことになり、sudoが使えなくなる。

……という認識で多分あっているはず(ubuntu素人なので実際の挙動がわからない、有識者の意見をお待ちしています)

この不具合を直すには、/etc/default/localeを書き換えて不正値を削除すれば良い。

ただし/etc/default/localeを書き換えるにはroot権限が必要

当然ながら/etc/default/localeは読み取り専用ファイルであるので、$vim /etc/default/localeではどうやっても書き換えられない。$sudo vim /etc/default/localeと宣言するとpamが不正値を読み込んでsudoが使えない。$su$sudo su,$sudo -iもパスワード認証が必要。パスワード認証が必要な処理なので当然動かない。

とんだハメ技じゃないか……

WSLのみ可能な解決方法

WSLはWindowsのサブシステムとしてリナックスを起動するやつである。つまりWindowsのほうが格上なのである(システムの権限的な話です)

Windows側のコマンドプロンプト、もしくはPowelShellを開き以下を記述する

> wsl -u root

これでrootユーザーとしてログインできる。よくわからんができた。そのまま

$ vim /etc/default/locale

して不正値を消し、:wqで保存終了できる。rootユーザーからログアウトした後はsudoは問題なく扱える。

結論

Ubuntuむずかしい

使って便利だったスプレッドシート関数の記録

つかった関数を覚え書き

IFNA

読んだ通りに#N/Aのときだけ後半の処理をする。#N/Aでないなら前半の値を表示する。

こんなふうにつかった

=IFNA(/* 長めの計算式が書いてある*/ , "0" )
 →有効な計算結果の値か0

CONCAT

CONCAT(A , B)はAとBの値を連結する。

こんなふうに使った
A B C D ...
1 東京 神奈川 千葉 岐阜 ...
2

=CONCAT(A$1,"*")
→東京*という文字列を得られる。*は検索時ワイルドカードであり、
 検索をすると東京○○と続く文字列を抽出できる。
 また、列番号+$+行番号とかくと、式をコピーしたときにB1,C1...と変化して便利。

INDEX(検索範囲,MATCH(行番号検索),MATCH(列番号検索))

f:id:amvirosa:20220108214340p:plain
f:id:amvirosa:20220108214405p:plain

説明が難しい。各列に対応するデータを揃えるために使った。

その他Tips

A$1 はコピーすると列番号のみ変動,行番号は1固定
$A1 は列がA固定、行番号は変動

縦にも横にも関連データが広がるシートを一行に整形する【GoogleAppsScript】

GoogleAppsScript書き始めて二日目。

言語仕様はそのままJavaScriptらしいがC++しかまともにやってないのでなんもわからん。

ただまあ、for文の書き方はC++とほぼ同じだしvar let constの仕様さえ覚えればC++のつもりで書けるみたいなのでなんもわからんが何故かマクロが動いたのでよし。

理想的なデータ構造

個別ID 名前 項目1 項目2 項目n...
1 りんご バラ科 しゃりしゃり etc...
2 ごりら ヒト科 ごりらごりら etc...
... ... ... ... ...

こういうデータだと個別IDで検索をかけるだけでほしいデータをすぐに取り出せて良い。

今回整形するデータ

個別ID 項目
1 りんご うまい
バラ科
しゃりしゃり
赤い
apple
2 ごりら
ごりらごりら
うほうほ
森の賢者
3 ピカソ
パブロ
ディエゴ
ホセ
...

タイトルの通り、縦と横に関連データが広がっていてとても使いにくい。

そもそもこんなデータを作らないでほしい。

手作業で解決するにはコピーして転置貼り付けして……を繰り返すしか無いが、

今回はマクロを組んで解決した。3000行ぐらいあるデータを手作業で整形できるわけがなかった。

1:個別IDが書き込まれている行番号の配列を作る

GAS(GoogleAppsScript)にはスプレッドシート操作に便利なAPIが用意されているのだが、

いちいちそれを呼び出していると実行速度が遅くなりあまり良くない。

そのため必要なデータをシート側の関数を用いて作っておく。

今回のデータはA列に唯一無二のIDが書き込まれており、

あるIDをもつセルの行番号から次のIDを持つセルの行番号-1までの範囲をコピーして転置したい、ということにする。

空いている列に=IF( ISBLANK(A1), ,ROW( ) )と入れてオートフィルで表の下まで入力

するとIDがある行番号が返されるので、フィルタを作成して空白を除外表示する

f:id:amvirosa:20220107184838p:plain

f:id:amvirosa:20220107185007p:plain

このままコピーして別シートに値のみ貼り付けをすると行番号配列が出来上がる。後でGAS側で読み込むのでシート名もわかりやすくリネームしておく

ついでに結果を書き込む新しいシートも用意しておくと失敗したときに大変なことにならなくて良い(3敗)

2:GoogleAppsScriptで書く

行番号配列row_indexs[i]とrow_index[i+1]の要素が元データでコピーしたい範囲をだいたい示している。

function trimDataToRow(){
 const read_file = SpreadsheetApp.openById("スプレッドシート固有ID");
 const row_indexs = read_file.getSheetByName("行番号配列があるシート名").getRange(/*行番号配列の範囲*/).getValues();
 const read_datas =read_file.getSheetByName("クソデータがあるシート名").getRange(/*転置したいデータがある範囲*/).getValues();

 const set_sheet = read_file.getSheetByName("書き込むシート名");

 for(let i=0;i<row_indexs.length-1;i++){
  let rows = { begin: row_indexs[i], end:row_indexs[i+1] };
  let temp_array = [];
  for(let j=rows.begin; j<rows.end-1; j++){
   temp_array.push(String(read_datas[j]));
  }
 set_sheet.appendRow(temp_array);
 }
}

実行するとこのようになる。

f:id:amvirosa:20220107195406p:plain

3:完成!

IDと同じ列はフィルタの空欄除外で抽出できる。

f:id:amvirosa:20220107195919p:plain

GASで作ったデータと組み合わせれば完成。

f:id:amvirosa:20220107200136p:plain

実行速度とかの話

クソデータの行数が4000近くあるデータでも実行時間制限にひっかからずに書き出せる。

ひっかからないのだが、処理が遅い。

f:id:amvirosa:20220107200850p:plainf:id:amvirosa:20220107201013p:plain

上記コードはfor(let i)のループ回数につき一回.appendRow();をしているのでそこがボトルネックになっている。

速度を求めるならループ中で二次元配列を作った上で,ループを抜けた後に.getRange(書き込む範囲).setValues(二次元配列);を一回呼ぶのがよい。

しかしgetRangeにわたす範囲の指定とsetValuesにわたす二次元配列の範囲の指定は完全に一致していないといけないらしく、

クソデータ、もといジャグ配列を渡すのが面倒くさく諦めた。

ポケモンの捕獲率をシミュレートするプログラムをつくるver0.01

BDSPが色々と面白く(そして世代なので懐かしく)ドハマリしているのでプログラミングの勉強をしながら遊ぶことにしました。
タイトル通りポケモンの捕獲率をシミュレートするプログラムを作ります。
言語はいつものC++(いつまでも勉強中)に加えて、OpenSiv3Dライブラリ(まだまだ勉強中)を使います。


あと、割といきあたりばったりに記事を書きながら進行形で書いていたので数値型が統一されて無くてバラバラだったりしました(パット見で直したけど直ってないところあるかも)

捕獲処理の計算式を知る

B=(((最大HP×3-現在HP×2)×捕捉率×捕獲補正)÷(最大HP×3))×状態異常補正

Bが255(FF)以上なら捕獲確定。 

捕捉率:捕獲するポケモンの捕まえやすさ。捕捉率一覧を参照。
捕獲補正:ボールの捕まえやすさ。捕獲補正率を参照。
状態異常補正:
こおり・ねむりなら2(第四世代以降では2.5)
どく・まひ・やけどなら1.5。

捕獲が確定しなかった場合、次にGを求める。

G=65535÷(4√(255÷B))
0~65535の乱数を発生させて、それが4回ともG以下なら捕獲成功。 Gより大きい乱数があった時点で失敗が確定し、それ以降揺れ判定は行われない。

wiki.xn--rckteqa2e.com

ポケモンwikiによるとこのような計算式のようです。
実際にBDSPの捕獲処理がこの通りなのかわかりませんが、とりあえずBであると仮定します。
またGはめんどくさいで無視し、Bの計算式が255以上なら確定であることを利用したプログラムをつくります。

捕獲処理式を関数にする

脳直で書いてみる(もちろんこのままでは問題がある)

float Pokemon_Capture_Method(Array<float>& Date) {
	const float MaxHP = Date[0] * 3;//Date[0]=任意のポケモンの最大HP 
	const float MinHP = Date[1] * 2;//Date[1]=ポケモンのHPをどれだけ減らすか
	const float CatchRATE = Date[2];//Date[2]=任意のポケモンの捕捉値
	const float BoalRATE = Date[3];//Date[3]=任意のボール補正値、ただしレベルボール、ヘビーボールは乗算ではなく加算処理
	const float StatusRATE = Date[4];//Date[4]=任意の状態異常補正値

	bool LEVELorHEAVY = false;//計算式を分岐しなければという意志の現れ,現状ただの飾り

	if (not LEVELorHEAVY) {
		return (((MaxHP - MinHP) * CatchRATE * BoalRATE) / MaxHP) * StatusRATE;
	}
	else {//レベルボール、ヘビーボールの場合
		return (((MaxHP - MinHP) * CatchRATE + BoalRATE) / MaxHP) * StatusRATE;
	}
}

とりあえず必要な引数は5つありそうなことがわかりました。
レベルボール、ヘビーボールの場合は処理が分岐しますがひとまず無視することにします。

この関数を動作させてみます。
例えば捕獲済みLV30メタモン(とりあえずHP80と設定)をHP1まで減らし、眠らせた上でリピートボールを投げる場合{80,1,35,3,2.5}です

void Main()
{
	Array<float>Metamon = { 80,1,35,3,2.5 };
	Print << U"メタモンを眠らせてみねうちしてリピートボールを投げるぜ!";
	Print << U"捕獲確率は" << Pokemon_Capture_Method(Metamon);
	while (System::Update())//メインループ
	{
	}
}

f:id:amvirosa:20211210102251p:plain
255以上なので捕獲確定だということが伺えます。やったね。

このままでは不便である。快適にするにはどうすれば?

現在のコードでは言うまでもなく不便です。具体的には

  1. 255以上が確定であることが直感的にわかりにくい
  2. 実HP入力が面倒
  3. 条件を満たす場合のボール補正と満たさない場合のボール補正を自動的に判断できない

1は人間に馴染みある100%表記に直すことでわかりやすくなるはずですし、実装もおそらく簡単です

2は実HP入力ではなくLV入力をユーザーに求め、個体値0の時の確率-個体値31の時の確率の範囲表記が良さそうです。
ポケモン固有のHP種族値のデータとLVの値からHP実数値を求める関数を実装すれば良さそう。実装はまあ出来なくもなさそう。

3はちょっと難易度が高そうなのでひとまず保留です。分岐が多すぎる……
面倒なガンテツボールを全部省略し、
各ボールの最大値のみ判定するならば簡単なので今回は妥協します。

100分率表記にする関数を実装

おそらくSTLにそういうものが見当たらなかったので自作します(あったらまあ……)
今までは特定の型だけに対応するだけの関数や、
複数型に対応するにしても同盟多重定義でちまちま書いていました。
が、c++のテンプレート機能を使えば全てが1つにまとまります。便利だが便利すぎて使いこなせない。

template<typename T>
T Percentage(T a,T b){
	return a / b * 100;	
}
//色々省略
void Main()
{
	Print << U"捕獲確率は" << Percentage(Pokemon_Capture_Method(Metamon), 255.0F) << U"%";
}

f:id:amvirosa:20211210112341p:plain
人間に優しい100分率表記にすることでいかにも捕獲確定していそうな感じになりました。

LVを入力したら実HPを計算してくれる関数を実装

まずポケモンのHP実数値の計算式を調べます。

ポケモンの能力値(ゲームのポケモン確認画面で表示されるステータス数値)は、HPとそれ以外の能力とで式が異なり、下記の計算式で決定されています。 なお、計算途中で小数点以下が生じる度に切り捨てされます。
HP
能力値=(種族値×2+個体値努力値÷4)×レベル÷100+レベル+10

yakkun.com
ポケモン徹底攻略によるとこのような計算式のようです。
浮動小数点型を一個も使わずに関数を組み立てれば良さそうですね。

uint16 Pokemon_ExistLV_HP(uint16 SyuzokuHP,uint16 Level,uint16 KotaiHP) {
	constexpr uint16 DoryokuHP = 0;//今回は野生ポケモンなので0
	uint16 Answer = ((((SyuzokuHP * 2) + KotaiValue + (DoryokuValue/ 4)) * Level / 100) + (Level + 10));
	return Answer;
}
//とんでメイン関数
	Print << U"レベル30のメタモンのHPを調べるぜ!";
	Print << Pokemon_ExistLV_HP(48, 30, 0) << U"-" << Pokemon_ExistLV_HP(48, 30, 31);

f:id:amvirosa:20211210114853p:plain

ポケモンの名前,HP種族値,捕捉率のデータを予め読み込んでおく処理

ポケモン種族値リスト(ポケモン徹底攻略)捕捉率一覧(ポケモンwiki)から必要な情報を抜き出したcsvファイルを作りました
docs.google.com
これを読み込みます。OpenSiv3Dの相対パスの基準は開発時はAppフォルダ基準なのでAppフォルダにcsvファイルをぶちこめば./hoge.csvで展開できます。

struct PokeDate {
	uint16 SyuzokuHP;
	uint16 CatchRATE;
};
void Main(){
	const CSV csv{U"./PokemonDate.csv"};//[0][0]-[493][3]//csvファイル読み込み
	if (not csv) {
		throw Error{ U"Failed to load `PokemonDate.csv`" };//読み込めなかった場合のエラー処理
	}
	HashTable<String, uint16>NameTable; //名前と図鑑Noが紐付いたハッシュテーブル(はやいらしい)
	Array<PokeDate>PokeValue;//i番目のデータ=図鑑No iのポケモンのHP種族値と捕捉率になる配列

	for (auto i : step(csv.rows())) {//csvのデータを配列にぶちこんでいく処理
		NameTable.emplace(csv[i][1], i);
		PokeDate kari;
		kari.SyuzokuHP = Parse<uint16>(csv[i][2]);
		kari.CatchRATE = Parse<uint16>(csv[i][3]);
		PokeValue << kari;
	}
	//NameTable.contains(U---);完全一致するテキストの検索;
	//NameTable[U--];一致テキストのキー(今回は図鑑No兼行番号);


	TextEditState tex01;
	String Kari = U"Nodate";

	// メインループ
	while (System::Update())
	{
		ClearPrint();
		if (NameTable.contains(Kari)) {//もし検索文字列がヒットするなら
			Print << U"図鑑No:" << NameTable[Kari];
			Print << U"HP種族値" << PokeValue[NameTable[Kari]].SyuzokuHP;
			Print << U"捕捉率" << PokeValue[NameTable[Kari]].CatchRATE;
		}
		else {
			Print <<U"テキストボックスにポケモン名を入力してね";
		}
		SimpleGUI::TextBox(tex01, Vec2{ 100, 140 });
		if (SimpleGUI::Button(U"検索", Vec2{ 320,100 }))
		{
			Kari = tex01.text;
			syokizyoutai = false;
		}
	}
}

f:id:amvirosa:20211210151027p:plain
もっと良いデータ構造の作り方が多分あると思いますが、
一応これでポケモン名を入力するだけで固有の数値を引っ張り出すことが出来ているのでヨシ!

そして色々あって完成した

githubで公開した
github.com

githubc++もopensiv3dも使い方わかんない、初心者なので…

いわゆる専用ブラウザはどのようにして作るのか【ざっくりした概論】

ぶっちゃけお船のゲームの専用ブラウザみたいなものを自作するには何をどうすればいいのだろう?

ということで大雑把に調べたことを書き記しておきます。

(実際の開発は行わないので内容が薄いです)

 

1.自作のウェブブラウザアプリを作る

これは難しくなく、Visual Studio2019でC#(.NET framework)の開発環境を整えると

Windowsフォームアプリの開発テンプレートが使えるので、それを使うと秒で完成します。

docs.microsoft.com

MicroSoft公式が解説しています。アプリケーションって意外と簡単に作れるんですね……(もっとこう、ボタンとかGUIもコードで記述しなきゃだと思っていた)

ボタンやチェックボックスを配置する雰囲気でウェブブラウザが配置できるので、

どうHTMLやCSSを解釈して描画するか問題は考えなくて良いです。

 

2.アプリに通信の中継をさせる(ローカルプロキシサーバーとしての機能をもたせる)&解析

というかローカルプロキシサーバーとして通信を仲介さえさせることができれば別に自作のブラウザアプリを作る必要はないのかもしれない。うん、ない。

例えばFireFoxなら特定のウェブサイトのみ指定したプロキシを経由させる拡張機能があるから、アプリにウェブブラウザ機能はあんまり必要ない。ということになる。

 

では中継するにはどうするか。C#.NETなんちゃらの場合Titanium Web Proxy

github.com

またはFiddler Core

www.telerik.com

Nekoxy

github.com

この3つのうちどれかを組み込むと、うまくできるらしい(試していない)

Fiddler Coreはなんかライセンスが個人利用のみ無料らしく、作ったアプリを配布しようとするとお金がかかるようだ。TitaniumとNekoxyはそうではない。

 

3.完成!

後はC#の基礎的な知識、プロキシ関連の基礎的な知識があれば完成する。

残念ながら私にはそのあたりの知識がないので完成しない。

記事ではさらっと流しているけれど頭の中は(プロキシ???ポート???)となっている。

 

基本情報技術者の試験勉強をするとそのへんの基礎知識が入荷できるかな~

とぼんやり考えている。

まずはC#の構文の勉強かな……

 

 

 

 

 

※彼の祖父は十分裕福であり、お年玉袋は十分に大きいものとする【ABC085C - Otoshidama】

AtCoderの問題文は独特なユニークさがあって好きです。

ABC085C - Otoshidama

atcoder.jp

やること

  • お札の枚数N,合計金額Y(Yは最大7桁)を受け取る
  • 10000x+5000y+1000z=Y かつ x+y+z=Nとなる組み合わせを1つでもいいので見つける
  • 組み合わせが見つかったらx,y,zの値を出力する
  • 見つからなかった時-1,-1,-1を出力する

普通に多重ループで解決できそうです。
zの値については、z=N-x-yで求められるのでzのループは省略できそう。
ただしz>=0ということを明示しないとダメ(コードテストでひっかかったところ)

解答 素直に書く

    #include <bits/stdc++.h>
    using namespace std;
     
    int main(){
      int N,Yen;
      cin >> N>>Yen;
      int x=0,y=0,z=0;
      int smalYen=Yen/1000;
      bool Ansflag=false;
      
      for(int i=0;i<=N;i++){
        for(int j=0;j<=N;j++){
          int kariZ=0;
          if(N-i-j>=0){
            kariZ=N-i-j;
          }
          if((10*i)+(5*j)+kariZ==smalYen && i+j+kariZ==N){
            Ansflag=true;
            x=i,y=j,z=kariZ;
          }      
        }
      }
      if(Ansflag){
      cout <<x <<" "<<y <<" "<<z <<endl;
      }else{
        cout << -1<<" "<< -1<<" "<< -1<<endl;
      }
     
    }
      

Y(en)は制約で1000の倍数であることが保証されているので、いつでも1000で割ることができます。
10000x+5000y+1000(N-x-y>=0)=smalY×1000となるので全ての項を1000で割って
10x+5y+(N-x-y>=0)=smalYを判定式として使えます。この式の条件を満たし、かつ
x+y+(N-x-y>=0)=Nという判定式を満たす時に組み合わせが存在することになります。

組み合わせを見つけたらi,j,N-i-jをそれぞれx,y,zに代入して、bool型のAnsflagをtrueにすることで、
Ansflagがtrueならx,y,zを表示、falseならば-1,-1,-1を表示させる分岐を作ります。

つまづき(コードテストで変なことになった)ポイントは
for文の条件式i<=Nをいつもの手癖でi

O(1)で解く方法があるらしい

x,y,zが全て0以上の正の整数であることなどを用いて連立方程式をこねくりまわすと導出できるらしい。
自力で導出できるように、いくつかの解説を読みながら自分で説明することで体系的な理解を試みる。

10x×10^3+5y×10^3+z×10^3=Y(n×10^3)
これをまず変形させて
10x+5y+z=Y'
ここまではできる。ここから、zを移項させた上で左辺の共通因数5をくくりだす
5(2x+y)=Y'-z\tag{1}
これが新しく導き出せる判定式、Y'-zは5の倍数であることが条件の1つになる。
また、合同式の性質の1つに
 a \equiv b(mod m) \iff a-b \equiv 0 (mod m)
つまりY'を5で割った余りとzを5で割った余りは等しい、ということになる。

またzが取りうる値の範囲について
0 \leq z \leq min(N,Y')\tag{2}
zは最大でもNとY'のどちらか小さい方までの値しかとることはない。
Y'-z=5(2x+y)
 N-z=x+y
であり、かつxとyは正の整数なので
 x+y \leq 2x+y \leq 2(x+y) \tag{3}
左辺値での不等式なので、これに右辺値(Y'-z),(N-z)を代入すると
 N-z \leq \dfrac{(Y'-z)}{5} \leq 2(N-z) \tag{3'}

(1)(2)(3')で用いる変数がzのみとなり(N,Y'は入力時に決定する定数)
この3つの式の条件に合うzが見つかる時に、xとyの値が一意に決まる(かつ、x,yも条件を満たしている)
ただ(3')が扱いにくいので、単純なzで不等式ができるように改変する必要がある。
数学苦手マンなのでかなり混乱したが、
まずはN-z≦(Y'-z)/5から1つずつ整理していくと
 5N-5z \leq Y'-z両辺に5を掛けて分母を払い、右辺のY’を消すために両辺に-Y’して
 5N-Y'-5z \leq -z左辺から-5zを消すために両辺に+5zを足す
5N-Y' \leq 4z最後に、zの係数を払うために両辺を4で割ることで
\frac{5N-Y'}{4} \leq zシンプルな不等式に整理できる。
これをY'-z/5≦2(N-z)についても同様に行う。*1
\dfrac{5N-Y'}{4} \leq z \leq \dfrac{10N-Y'}{9}  \tag{4}



このへんで計算量でパソコンが爆発するより先に私の頭が爆発した。

(追記) 一応形はできたがfor文に無駄を感じる

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int main(){
  ll N,Y;
  cin >>N>>Y;
  ll Ydash=Y/1000;//Y'
  
  ll min_N_Yd=min(N,Ydash);//最大値の最低ラインを決定

  ll modz=Ydash%5;//Ydash%5とz%5(両者を5で割った余り)は等しい  
  ll sahen=(5*N-Ydash)/4;//判定3の最低ライン
  ll uhen=(10*N-Ydash)/9;//判定3の最大ライン
  bool ans=false;
  
  for(int z=modz;z<=min_N_Yd;z+=5){
      bool flag1=(0<=z&&z<=min_N_Yd);//判定式1
      bool flag2=(z%5==modz);//判定式2
      bool flag3=(sahen<=z&&z<=uhen);//判定式3
      if(flag1 && flag2 && flag3){
        int x=0;
        int y=0;
        int siki=((Ydash-z)/5)-(N-z);
        if(siki>=0){
          x=siki;
        }
        y=N-z-x;
        cout << x << " "<< y << " "<< z <<endl;
        ans=true;
        return 0;
      }
     }
     if(ans==false){
       cout << -1 <<" "<<-1<<" "<<-1<<endl;
     }
}

一応ACを取れる。二重ループの実行時間が14msだったところ、こちらは6msとかなり早い。
for文をmod式の余りから始めて+5ずつ増やしているが、for文自体が無駄のような気がする。
どう消せばいいかまではちょっと考えられない(すでに頭が爆発しているため)
判定式3の範囲の最大値が0ならばzは0だし、判定式3の最低値以上かつmod5がY'と等しい値から始まるならばそれがzにすればいい。
(判定式3<=0)のときz=0,0<=判定式3のとき、判定式3の範囲の任意のY'≡z mod5を1つ選ぶ、という処理で
xとyは求められる、はず
stlに範囲と条件を渡せば返してくれる関数はあるとおもう
今日はもうむり

*1: (Y'-z/)5≦2N-2z → Y’-z≦10N-10z →-z≦10N-10z-Y' →9z≦10N-Y' →z≦(10N-Y')/9