最初にダブらないパズルゲームのステージ初期化の作り方

パズルゲームの初期ステージは完全ランダムだと好ましくない場合があります。

今回のブログでは縦横3つ揃わないようにするという条件でサンプルと考え方を紹介したいと思います。

ソースコード

import random


def generate_array(width: int, height: int, between:tuple[int]) -> list[int]:
    # width x heightの二次元配列を作成し、-1で初期化
    arr = [[-1 for _ in range(width)] for _ in range(height)]

    # 0から5までの数字をランダムに割り当てる
    for i in range(height):
        for j in range(width):
            while True:
                num = random.randint(between[0], between[1])
                if not is_continuous(arr, i, j, num):
                    arr[i][j] = num
                    break

    return arr


def is_continuous(arr, i, j, num):
    # 縦方向に3つ以上連続しているかをチェック
    if i >= 2 and arr[i - 1][j] == num and arr[i - 2][j] == num:
        return True
    # 横方向に3つ以上連続しているかをチェック
    if j >= 2 and arr[i][j - 1] == num and arr[i][j - 2] == num:
        return True
    return False


# 生成した二次元配列の表示
arr = generate_array(width=7, height=9, between=(1,5))
for row in arr:
    print(row)

下記は出力結果

> $ python initial_stage.py
[2, 4, 5, 5, 4, 1, 1]
[4, 2, 2, 3, 5, 3, 2]
[4, 5, 4, 2, 3, 4, 1]
[2, 3, 4, 2, 2, 5, 1]
[5, 3, 5, 4, 4, 1, 2]
[1, 2, 4, 5, 1, 3, 5]
[5, 3, 2, 3, 2, 1, 4]
[4, 1, 5, 1, 4, 4, 2]
[1, 4, 4, 3, 3, 5, 2]

> $ python initial_stage.py
[3, 5, 1, 5, 3, 4, 4]
[5, 1, 3, 4, 4, 1, 4]
[1, 3, 2, 1, 3, 3, 2]
[3, 5, 5, 1, 2, 4, 4]
[3, 5, 5, 2, 2, 1, 3]
[1, 4, 2, 2, 5, 4, 3]
[2, 1, 2, 5, 4, 5, 5]
[5, 5, 4, 3, 5, 3, 4]
[1, 1, 2, 5, 3, 1, 3]

解説

次のようなフローで作られています。

  1. 縦、横、数字の範囲(今回は1〜5)を指定します。
  2. 縦x横の大きさの二次元配列を-1で初期化して作成します。
  3. それぞれの値に1~5を割り当てます
    1. この時に左方向、または上方向に二つずつの値を取得し同じだった場合は再抽選します
    2. 左から2つ目まで、上から二つまでは判定できないので被りがないものとしています
  4. 全ての数字を入力できたら出来上がった二次元配列を返す

なお、今回の判定は縦か横なので、ぷよぷよみたいにギザギザでくっついているものは判定できてません。この場合はis_continuousの判定式を変更する必要があります。

懸念

  • 割り当てが不可能な場合、無限ループになる

今日はここまで、またお会いしましょう。

エラーについての考え方

今日は箇条書きになります。

  • 基本的にはエラーが発生したら即座にクラッシュさせる方が影響が少ない
  • 自前でキャッチしたりすると例外を握りつぶす可能性がある
  • 再スローするやり方をしているところもあるが、元のコードで新しくスローするコードが出てくるとその例外をスルーすることになってあまり良いコードとは言えない
  • これは個人的な意見だけど、英語が読めないからスローすることでわかりやすいメッセージにしようという親切心で再スローしている場合もありそうな気がする。(昔やってた)
  • 翻訳ソフトでメッセージを翻訳すれば、その問題はそんな考えなくてOK

今日はこんなところで、またお会いしましょう。

pythonで事前条件、事後条件の取り入れる方法の一例

私がよく読んでいる達人プログラマーの本の中に契約による設計という章があります。

こちらの内容をCacapon的な解釈だと受け取るデータ、渡すデータ、内部のデータに条件をつけて、関数やクラスが扱うデータが明確にするための考え方だと思っています。

契約による設計によると、三つの要素があります。

  • 事前条件:渡されるデータに対して設ける条件。f(x)のxは8文字までとか
  • 事後条件:処理が終わった後の返り値に設ける条件。y = f(x) の時、yは-になってはいけないとか
  • クラス不変表明:これはよくわかっていないんですけど、メンバー変数の正しさを保証するための条件だと思っています。例えば、クラスXのプロパティa は常に+の整数値であるなど

基本的にこの条件が守れない場合、例外を投げたり処理を終了することで条件内でしか動作しないことを保証するようにします。こうすることで安全なコードになるのでCacaponも達人プログラマーの考えの中ではかなり好きな部類でコードを書くときは使わせてもらっています。

ただ、言語レベルでサポートされているのは Eiffel やClojureなど一部の言語のみであり、私がよく使うpythonではサポートされていません。

なので、多少片手落ちな部分はありますが、とりあえず概念は理解できている事前条件、事後条件をチェックする関数を作ってみました。クラス不変表明はまた別の日にでも。

conditon.py
def pre(condition:bool, message:str):
    if not condition: raise AssertionError('err:pre: ' + message)


def post(condition:bool, message:str):
    if not condition: raise AssertionError('err:post: ' + message)

コード自体はシンプルですね。ちなみにpreが事前条件、postが事後条件です。*1

conditionに判定を行うような式を入れて、結果がFalseだったらエラーを発行する、という流れになります。messageには失敗した時に表示したいメッセージを設定できるようにしてします。

raise は例外を発行するためのコマンドです。ここではAssetionErrorというエラーが発行されるようになっています。

作成した事前条件、事後条件の動作確認用のサンプルはこちらになります。

add.py
from conditon import pre, post


def add(a, b):
    pre(0 < a < 10, f"aは1~9にしてください a:{a}")
    pre(0 < b < 10, f"bは1~9にしてください b:{b}")

    result = a + b

    post(0 <= result < 10, f"resultは自然数の一桁にしてください result:{result}")
    return result


for a in range(10):
    for b in range(10):
        try:
            print(f"{a} + {b} = {add(a,b)}")
        except AssertionError as e:
            print(e)

こちらのadd関数は入力を1~9しか受け付けず、さらに出力する結果は一桁である必要があるように条件をつけています。

出力結果がこちら

$ python3 app.py                                                                                                                                                                                                      
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: aは1~9にしてください a:0
err:pre: bは1~9にしてください b:0
1 + 1 = 2
1 + 2 = 3
1 + 3 = 4
1 + 4 = 5
1 + 5 = 6
1 + 6 = 7
1 + 7 = 8
1 + 8 = 9
err:post: resultは自然数の一桁にしてください result:10
err:pre: bは1~9にしてください b:0
2 + 1 = 3
2 + 2 = 4
2 + 3 = 5
2 + 4 = 6
2 + 5 = 7
2 + 6 = 8
2 + 7 = 9
err:post: resultは自然数の一桁にしてください result:10
err:post: resultは自然数の一桁にしてください result:11
err:pre: bは1~9にしてください b:0
3 + 1 = 4
3 + 2 = 5
3 + 3 = 6
3 + 4 = 7
3 + 5 = 8
3 + 6 = 9
err:post: resultは自然数の一桁にしてください result:10
err:post: resultは自然数の一桁にしてください result:11
err:post: resultは自然数の一桁にしてください result:12
err:pre: bは1~9にしてください b:0
4 + 1 = 5
4 + 2 = 6
4 + 3 = 7
4 + 4 = 8
4 + 5 = 9
err:post: resultは自然数の一桁にしてください result:10
err:post: resultは自然数の一桁にしてください result:11
err:post: resultは自然数の一桁にしてください result:12
err:post: resultは自然数の一桁にしてください result:13
err:pre: bは1~9にしてください b:0
5 + 1 = 6
5 + 2 = 7
5 + 3 = 8
5 + 4 = 9
err:post: resultは自然数の一桁にしてください result:10
err:post: resultは自然数の一桁にしてください result:11
err:post: resultは自然数の一桁にしてください result:12
err:post: resultは自然数の一桁にしてください result:13
err:post: resultは自然数の一桁にしてください result:14
err:pre: bは1~9にしてください b:0
6 + 1 = 7
6 + 2 = 8
6 + 3 = 9
err:post: resultは自然数の一桁にしてください result:10
err:post: resultは自然数の一桁にしてください result:11
err:post: resultは自然数の一桁にしてください result:12
err:post: resultは自然数の一桁にしてください result:13
err:post: resultは自然数の一桁にしてください result:14
err:post: resultは自然数の一桁にしてください result:15
err:pre: bは1~9にしてください b:0
7 + 1 = 8
7 + 2 = 9
err:post: resultは自然数の一桁にしてください result:10
err:post: resultは自然数の一桁にしてください result:11
err:post: resultは自然数の一桁にしてください result:12
err:post: resultは自然数の一桁にしてください result:13
err:post: resultは自然数の一桁にしてください result:14
err:post: resultは自然数の一桁にしてください result:15
err:post: resultは自然数の一桁にしてください result:16
err:pre: bは1~9にしてください b:0
8 + 1 = 9
err:post: resultは自然数の一桁にしてください result:10
err:post: resultは自然数の一桁にしてください result:11
err:post: resultは自然数の一桁にしてください result:12
err:post: resultは自然数の一桁にしてください result:13
err:post: resultは自然数の一桁にしてください result:14
err:post: resultは自然数の一桁にしてください result:15
err:post: resultは自然数の一桁にしてください result:16
err:post: resultは自然数の一桁にしてください result:17
err:pre: bは1~9にしてください b:0
err:post: resultは自然数の一桁にしてください result:10
err:post: resultは自然数の一桁にしてください result:11
err:post: resultは自然数の一桁にしてください result:12
err:post: resultは自然数の一桁にしてください result:13
err:post: resultは自然数の一桁にしてください result:14
err:post: resultは自然数の一桁にしてください result:15
err:post: resultは自然数の一桁にしてください result:16
err:post: resultは自然数の一桁にしてください result:17
err:post: resultは自然数の一桁にしてください result:18

実行結果は0+0から9+9までの結果になります。入力が10以上の時は確認で来てませんが、入力が0の時、結果が10以上の時はエラーを吐くようになっていますね。無事に事前条件、事後条件をチェックする機能を作ることができました。何かの参考になったら幸いです。また別のブログでお会いしましょう。







…ここからは余談なのですが、pythonにはassertという判定値がFalseだったらAssertionErrorを出してくれるキーワード文があります。

assert 0 == 1 , "エラー!" # 0==1はFalseなのでAssertionErrorになる

これってまんま、pre,postで判定している式ですし、みた感じと同じなのですが、とある理由でわざわざassert文を使っていません。

その理由なのですが、デバッグフラグがFalseだと実行時にオフになっちゃうんです。

例えば、python3 -0 app.pyのように実行すると、assertキーワードで作成した部分は無視されるのがわかると思います。

python以外の言語でもassertのような機能ってデバッグ専用みたいなニュアンスが多いみたいで、ビルドすると無くなったりする場合もありますので注意です。*2

これはCacaponの考えなので賛否両論は承知の上ですが、デバッグで大丈夫だからとassertのようなチェックをする機能を本番でオフにするのはナンセンスだと考えています。

例えばですが、もしpre,postがassertで作られていた場合でadd関数を本番運用すると、入力が-9だろうが、結果が18だろうがエラーになりません。これって想定外の入力も受け付けるってことになるのでどんな挙動になるか運用している人もプログラム作った人も把握できてないプログラムになってしまうんです、これって危ないですよね?

それなら、すぐエラーになって落ちるプログラムのほうがわかりやすいですし安全です。なので、cacaponはassertは基本的に使わず、さっきのような自作で例外を出すコードを書いています。assert使うのはpytestぐらいでしょうか?

…なんか、書いてたら余談の方が長くなってしまった気がします笑

本日は本当にここまでです、ここまで読んでいただきありがとうございました。

*1:DeepLで翻訳したところpreconditionが事前条件、postconditionが事後条件だったので頭文字のpre,postで作りました。

*2:確かC#で似たようなところで困った記憶があります

1分以内にaws lambdaでファイルをマージする方法

AWSのLambdaで S3にある複数のファイルを一つにマージして別のS3に置く、ただし1分以内で

こんな仕組みを実現するために考えたことを共有しようと思います。



このブログを読むと分かること

  • 1分以内に4000ファイルを1つのファイルにまとめる方法

前提条件

  • S3がファイルを格納するサービスなんだなぁというのがふんわりわかっていること
  • Lambdaが関数を実行するサービスなんだなぁというのがふんわりわかっていること
  • unixベースのシェルコマンドを実行するとどんな感じになるか少しは分かること
  • pythonのコードがある程度なら読めること

イメージ

こんな感じのを1分以内で処理完了するようにつくりたい

S3には大量のファイルが送られており、1分あたり最大で4000ファイルが格納されるのを想定しています。

このファイルは1ファイルあたり2kbくらいのサイズのものです。

こちらのファイルは後続で別の処理をするのに使うのですが、一つずつ読み込んで処理するのに時間がかかるため、ひとまとめにする処理を挟むことになりました。

こちらの4000ファイルを1分以内に1ファイルに一まとめにし、そのファイルをS3にアップロードする必要があります。

どうすればよいでしょうか?

対応

次の3つの視点で考えていきます。

  • S3から4000ファイルをダウンロードする
  • ダウンロードした4000ファイルを一つにまとめる
  • まとめた一つのファイルをS3にアップロードする

S3から4000ファイルをダウンロードする

pythonにはboto3というAWSのコマンドを扱えるライブラリがあるのですが、大量のファイルを短時間で取得するコマンドが見つからなかったので、aws-cliをlambda上で扱えるようにして、s3 syncコマンドでダウンロードするというアプローチでダウンロードを行いたいと思います。

aws-cliをlambda上で扱えるようにする方法は下記を参考に実施しました。

www.bioerrorlog.work

こちらの設定がうまくできると、/opt/awsからアクセスできるようになるので、python上でCLIコマンドを実行できるsubprocess.run()を使ってダウンロードしましょう。

subprocess.run(f'/opt/aws s3 sync s3://cacapon-sandbox-s3/test /tmp/input/{output_dir}', shell=True)

上記を実行すると、s3 sync で cacapon-sandbox-s3/test オブジェクト下のファイルを全てダウンロードし、/tmp/input/{output_dir} 直下にダウンロードしたファイルを置くことができます。

最後のにくっついているshell=Trueは渡した文字列がシェルコマンドですよと認識してもらうための設定です。

これを実行すると、1GBメモリで37秒, 2GBで19秒前後でダウンロードされます。

ダウンロードした4000ファイルを一つにまとめる

今回はcatコマンドで実施しました。python上で行う場合は同じくsubprocess.run()です。

catコマンドは指定したファイルの中身を表示するコマンドですが、ワイルドカード指定すると対象のファイルを全部くっつけた状態で出力されます。

subprocess.run(f'cat /tmp/input/{output_dir}/* > /tmp/output/merge_yyyymmdd_hhmmssfff.csv', shell=True)

こちらは自前の環境でコマンドを実行したところ、0.091秒でした

まとめた一つのファイルをS3にアップロードする

まとめたファイルのアップロードはダウンロードと同じくs3 syncを使用しました。

s3 sync はローカルファイル S3のアップロード先 と指定するとファイル丸ごとアップロードできるようになります。

subprocess.run(f'/opt/aws s3 sync /tmp/output/ s3://cacapon-sandbox-s3/output/', shell=True)

こちらで実行すると1GBで2.5秒、2GBで1.5秒ほどで実行できました。

まとめ

最終的なコードは以下のようになりました。

import uuid
import subprocess
import logging
import boto3

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def main():
    output_dir = str(uuid.uuid4())
    # download
    subprocess.run(f'/opt/aws s3 sync s3://cacapon-sandbox-s3/test /tmp/input/{output_dir}', shell=True)

    # merge
    subprocess.run('mkdir /tmp/output', shell=True)
    subprocess.run(f'cat /tmp/input/{output_dir}/* > /tmp/output/merge_yyyymmdd_hhmmssfff.csv', shell=True)

    # upload
    logger.info("upload start")
    subprocess.run(f'/opt/aws s3 sync /tmp/output/ s3://cacapon-sandbox-s3/output/', shell=True)
    logger.info("upload end")

    # 片付け
    subprocess.run('rm -rf /tmp/input/{output_dir}/', shell=True)
    
    # 確認
    subprocess.run(f'ls /tmp/input/{output_dir}/', shell=True)
    subprocess.run('stat /tmp/output/merge_yyyymmdd_hhmmssfff.csv', shell=True)

def lambda_handler(event, context):
    main()

前述のようにlambdaのメモリ設定で時間は異なりますが、1GBでおおよそ37秒、2GBで19秒前後で処理を完了できますので、無事1分以内に収まりました。

今回は結果だけ書いたのでここに至るまでの経緯は省いてしまったのですが、for文の繰り返しだと間に合わない、並列処理だとpythonやlambdaの制約でうまくいかなかったりと苦戦した部分も多々ありました。その話はいずれ別にお話しできればと思います。

本日のブログはここまで、またお会いしましょう。

今振り返ってみて、初心者はどんな感じでプログラミングを学ぶと良いか?

箇条書きでまとめてみます

  • まず一つの言語をマスターする
    • 日本語分かると関西弁が少し分かるみたいなニュアンスで他の言語でも応用が効くようになったりします。
  • 基本的なところを押さえている本を読む
  • サンプルを写経する
    • コピペじゃなくて写経
    • 一つずつ動かしていくと理解が深まります
  • アルゴリズムを学ぶ
    • 効率性を求めるときは既存のアルゴリズムを学ぶことで挙げられると思います
  • ユニットテストを書く
    • 自分のコードに自信が持てるようになります。
    • 説明する時の根拠になります

今日はこんなところで、またお会いしましょう。

学習について考える

最近もっと効率よく学習できるようにするにはどうしたら良いか考えているCacaponです。

NLPによると「感情のインパクトが強い出来事」か「繰り返される出来事」は記憶されるらしいです。


感情のインパクトが強い出来事は、例えば感動した出来事とか、トラウマ、恥ずかしい出来事とかですかね。

Cacaponも中学2年の市大会新人戦の卓球大会で優勝したときの記憶は今でも鮮明に残っています。こういった感情が強く働いた出来事は一回だけでも一発で長く覚えてられますね。


「繰り返される出来事」はこれは結構わかりやすいかもです。私の場合、最近の例だと、英単語を覚えた話がありますね。

Target1900の英単語の最初5単語だけ音声、和訳、即興で短文作りをひたすら繰り返してたら、ふとしたタイミングでもcreate,implove, increase, mean, own はすんなり言えるようになりましたし、意味も理解しています。(綴りは間違っているかも?)

そこから調子乗って25単語一気にやったら何にも覚えていませんが笑


このことから考えると、比較的コントロールしやすい繰り返し行う、というのを学習に取り組むと効率が上がるのかもしれませんね。感情のコントロールが上手い人は覚えたいことと強い感情を紐づけるのもいいかもしれませんが、Cacaponはすぐできそうにはないです…

しばらく学習についてもう少し考えてみようと思います、それではまた。

ロジック部分と実際に見える部分の分離についてまだ身についていない話

今日は、ぼやき程度のブログです笑

ゲーム開発をしている時、実際に見えるアニメーションの部分とロジック部分は基本的に別物で考える必要があります。

Cacaponはロジック部分はある程度作れるんですけど、アニメーション部分との兼ね合い部分があまり得意ではありません。

できるようになりたいなぁとは思っているのですが、まだ無意識レベルまで落とし込めていない感じです。

今まで作ったゲームも落ちものパズルもカクカク落ちる感じにはできるんですけど、滑らかな落とし方ができない、みたいな感じです。

前にぷよぷよの実装を見たことあるんですけど、それを見ると状態遷移を持たせて、アクションがあったらアニメーションをする、その間は入力を受け付けない、みたいな形で実現しているみたいです。

Cacaponもこれを習って1マス移動するアニメーションから作って行ってレベルアップしていこうかなと思います。

今日はこの辺で、またお会いしましょう。