初心者がUE4でルービックキューブ風なものを作って強化学習用のライブラリにしたい #6

前回に引き続き初心者がUE4を絡めたPythonライブラリを作れないか色々進めていく記事です（ほぼ自分のための備忘録としての面が強い・・）。

最初 : [#1](https://qiita.com/simonritchie/items/e8d84c2941b5bd2862f5)
前回 : [#5](https://qiita.com/simonritchie/items/f276a207da295e4ca581)

## Observation 回りの実装をしていく

観測値回りの制御の処理を書いていきます。
基本的にアクションの後に保存などの処理が必要になるのと、用意したSQLiteのテーブルにアクションのIDのカラムを用意してあるので、BPと繋げるところはアクションのモジュールに追加していきます（別途Observation用のPyActorを使ったモジュールを追加しようとも思いましたが、アクション関係との連携が無駄に煩雑になるのでアクションの方に追加していきます）。
ただし、BPが絡まないところの処理は別途汎用モジュールを用意してそちらに記載していきます。

BP_Actionにオレンジ色の平面のワールド基準の座標を取得する処理を追加します。すでに座標取得の処理は以前の記事で追加したため、ここではほぼインターフェイスのみです。とりあえずオレンジ面のみ対応し、他は確認を取ってから進めます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/3da3bd08-4d72-d864-5f5c-2d0f3415628e.png)

Python側のクラスで、先ほど用意したBPの関数と繋げるための関数を用意します。

```py:Content\Scripts\action.py
    def get_orange_plane_world_locations(self):
        """
        オレンジ色の平面のワールド基準の各座標値の取得を行う。

        Returns
        ----------
        x_list : list of int
            ワールド基準のX座標のリスト。
        y_list : list of int
            ワールド基準のY座標のリスト。
        z_list : list of int
            ワールド基準のZ座標のリスト。
        """
        x_list, y_list, z_list = self.uobject.getOrangePlaneWorldLocations()
        return x_list, y_list, z_list
```

また、observation_utils.pyというモジュールを追加し、そちらに定数定義や汎用の関数などを追加していきます（全部の面だと長いので一部のみ記載）。

```py:Plugins\UnrealEnginePython\Binaries\Win64\common\observation_utils.py
"""観測値関係の共通の処理などを記述したモジュール。
"""

from common import sqlite_utils


# -----------------------------------------------------------------------
# 前面の平面の座標値の定義。

FRONT_POS_X_1_1 = -151
FRONT_POS_X_2_1 = -151
FRONT_POS_X_3_1 = -151
FRONT_POS_X_1_2 = -151
FRONT_POS_X_2_2 = -151
FRONT_POS_X_3_2 = -151
FRONT_POS_X_1_3 = -151
FRONT_POS_X_2_3 = -151
FRONT_POS_X_3_3 = -151

FRONT_POS_X_LIST = [
    FRONT_POS_X_1_1,
    FRONT_POS_X_2_1,
    FRONT_POS_X_3_1,
    FRONT_POS_X_1_2,
    FRONT_POS_X_2_2,
    FRONT_POS_X_3_2,
    FRONT_POS_X_1_3,
    FRONT_POS_X_2_3,
    FRONT_POS_X_3_3,
]

FRONT_POS_Y_1_1 = -100
FRONT_POS_Y_2_1 = -100
FRONT_POS_Y_3_1 = -100
FRONT_POS_Y_1_2 = 0
FRONT_POS_Y_2_2 = 0
FRONT_POS_Y_3_2 = 0
FRONT_POS_Y_1_3 = 100
FRONT_POS_Y_2_3 = 100
FRONT_POS_Y_3_3 = 100

FRONT_POS_Y_LIST = [
    FRONT_POS_Y_1_1,
    FRONT_POS_Y_2_1,
    FRONT_POS_Y_3_1,
    FRONT_POS_Y_1_2,
    FRONT_POS_Y_2_2,
    FRONT_POS_Y_3_2,
    FRONT_POS_Y_1_3,
    FRONT_POS_Y_2_3,
    FRONT_POS_Y_3_3,
]

FRONT_POS_Z_1_1 = 100
FRONT_POS_Z_2_1 = 0
FRONT_POS_Z_3_1 = -100
FRONT_POS_Z_1_2 = 100
FRONT_POS_Z_2_2 = 0
FRONT_POS_Z_3_2 = -100
FRONT_POS_Z_1_3 = 100
FRONT_POS_Z_2_3 = 0
FRONT_POS_Z_3_3 = -100

FRONT_POS_Z_LIST = [
    FRONT_POS_Z_1_1,
    FRONT_POS_Z_2_1,
    FRONT_POS_Z_3_1,
    FRONT_POS_Z_1_2,
    FRONT_POS_Z_2_2,
    FRONT_POS_Z_3_2,
    FRONT_POS_Z_1_3,
    FRONT_POS_Z_2_3,
    FRONT_POS_Z_3_3,
]


# -----------------------------------------------------------------------
# 左面の平面の座標値の定義。

LEFT_POS_X_1_1 = 100
LEFT_POS_X_2_1 = 100
LEFT_POS_X_3_1 = 100
LEFT_POS_X_1_2 = 0
LEFT_POS_X_2_2 = 0
LEFT_POS_X_3_2 = 0
LEFT_POS_X_1_3 = -100
LEFT_POS_X_2_3 = -100
LEFT_POS_X_3_3 = -100
...


# -----------------------------------------------------------------------
# 位置の座標と位置の種別値のクラス定義。

class _PositionAndPositionNumLists:

    def __init__(
            self, pos_x_list, pos_y_list, pos_z_list, position_num_list):
        """
        各平面の座標とそれに紐づく位置の種別値のリストを属性に持つクラス。
        各リストのインデックスの内容は一致させる必要がある。

        Parameters
        ----------
        pos_x_list : list of int
            対象の平面のX座標のリスト。
        pos_y_list : list of int
            対象の平面のY座標のリスト。
        pos_z_list : list of int
            対象の平面のZ座標のリスト。
        position_num_list : list of int
            対象の位置の種別値を格納したリスト。
        """
        self.pos_x_list = pos_x_list
        self.pos_y_list = pos_y_list
        self.pos_z_list = pos_z_list
        self.position_num_list = position_num_list


FRONT_POS_AND_POS_NUM_LISTS = _PositionAndPositionNumLists(
    pos_x_list=FRONT_POS_X_LIST,
    pos_y_list=FRONT_POS_Y_LIST,
    pos_z_list=FRONT_POS_Z_LIST,
    position_num_list=sqlite_utils.POSITION_NUM_FRONT_LIST,
)

LEFT_POS_AND_POS_NUM_LISTS = _PositionAndPositionNumLists(
    pos_x_list=LEFT_POS_X_LIST,
    pos_y_list=LEFT_POS_Y_LIST,
    pos_z_list=LEFT_POS_Z_LIST,
    position_num_list=sqlite_utils.POSITION_NUM_LEFT_LIST)

RIGHT_POS_AND_POS_NUM_LISTS = _PositionAndPositionNumLists(
    pos_x_list=RIGHT_POS_X_LIST,
    pos_y_list=RIGHT_POS_Y_LIST,
    pos_z_list=RIGHT_POS_Z_LIST,
    position_num_list=sqlite_utils.POSITION_NUM_RIGHT_LIST)

TOP_POS_AND_POS_NUM_LISTS = _PositionAndPositionNumLists(
    pos_x_list=TOP_POS_X_LIST,
    pos_y_list=TOP_POS_Y_LIST,
    pos_z_list=TOP_POS_Z_LIST,
    position_num_list=sqlite_utils.POSITION_NUM_TOP_LIST)

BACK_POS_AND_POS_NUM_LISTS = _PositionAndPositionNumLists(
    pos_x_list=BACK_POS_X_LIST,
    pos_y_list=BACK_POS_Y_LIST,
    pos_z_list=BACK_POS_Z_LIST,
    position_num_list=sqlite_utils.POSITION_NUM_BACK_LIST)

BOTTOM_POS_AND_POS_NUM_LISTS = _PositionAndPositionNumLists(
    pos_x_list=BOTTOM_POS_X_LIST,
    pos_y_list=BOTTOM_POS_Y_LIST,
    pos_z_list=BOTTOM_POS_Z_LIST,
    position_num_list=sqlite_utils.POSITION_NUM_BOTTOM_LIST)

POS_AND_POS_NUM_LISTS = [
    FRONT_POS_AND_POS_NUM_LISTS,
    LEFT_POS_AND_POS_NUM_LISTS,
    RIGHT_POS_AND_POS_NUM_LISTS,
    TOP_POS_AND_POS_NUM_LISTS,
    BACK_POS_AND_POS_NUM_LISTS,
    BOTTOM_POS_AND_POS_NUM_LISTS,
]


def get_position_num_by_world_location(world_x, world_y, world_z):
    """
    ワールド基準の座標に応じた、位置の種別値を取得する。

    Parameters
    ----------
    world_x : int
        対象のワールド基準のX座標。
    world_y : int
        対象のワールド基準のY座標。
    world_z : int
        対象のワールド基準のZ座標。

    Returns
    ----------
    position_num : int
        対象の位置の種別値。該当するものが存在する場合には1～54の値
        が設定される。

    Raises
    ----------
    ValueError
        対応する座標の定義が存在しない場合。
    """
    for unit_pos_and_pos_num_lists in POS_AND_POS_NUM_LISTS:
        pos_x_list = unit_pos_and_pos_num_lists.pos_x_list
        pos_y_list = unit_pos_and_pos_num_lists.pos_y_list
        pos_z_list = unit_pos_and_pos_num_lists.pos_z_list
        for i, (pos_x, pos_y, pos_z) in \
                enumerate(zip(pos_x_list, pos_y_list, pos_z_list)):
            if pos_x != world_x or pos_y != world_y or pos_z != world_z:
                continue
            position_num = unit_pos_and_pos_num_lists.position_num_list[i]
            return position_num
    err_msg = '対応する座標の定義が存在しません。X: %s, Y: %s, Z: %s'\
        % (world_x, world_y, world_z)
    raise ValueError(err_msg)

```

多くの定数定義を追加しましたが、結構値のうっかりミスなどをしがちなので、チェック用の処理をテストに追加しておきます。

```py:Plugins\UnrealEnginePython\Binaries\Win64\common\tests\test_observation_utils.py
"""observation_utils モジュールのテスト用のモジュール。
"""

import importlib
import inspect
from collections import defaultdict

from nose.tools import assert_equal, assert_raises

from common import observation_utils

importlib.reload(observation_utils)


def _check_all_values_are_same(target_list, const_prefix, expected_val):
    """
    対象のプリフィクスを持つ定数と、定数のリストの値が想定された値に
    なっていることを確認する。

    Parameters
    ----------
    target_list : list of int
        チェック対象の座標を格納した配列。
    const_prefix : str
        定数名のプリフィクス。
    expected_val : int
        想定される座標値。

    Raises
    ----------
    AssertionError
        - 対象のリスト内の値が想定値になっていない場合。
        - 対象のプリフィクスの定数が想定値になっていない場合。
    """
    for pos_val in target_list:
        assert_equal(pos_val, expected_val)
    members = inspect.getmembers(observation_utils)
    for obj_name, obj_val in members:
        if not obj_name.startswith(const_prefix):
            continue
        if not isinstance(obj_val, int):
            continue
        assert_equal(obj_val, expected_val)


def _check_values_are_three_sequence(
        target_list, const_prefix, expected_three_vals):
    """
    対象の定数のリストの値が同一の3つの連続した値になっている
    ことと、指定のプリフィクスの定数の値が3つの想定値で3件ずつ
    存在することをチェックする。

    Parameters
    ----------
    target_list : list of int
        チェック対象の座標を格納した配列。
    const_prefix : str
        定数名のプリフィクス。
    expected_three_vals : list of int
        想定される3つの値のリスト。

    Raises
    ----------
    AssertionError
        - リスト内の値が3つの連続した想定値になっていない場合。
        - 指定のプリフィクスを持つ定数が、想定値の3つの3つずつの
            値になっていない場合。
    """
    for pos_val in target_list[:3]:
        assert_equal(
            pos_val, expected_three_vals[0],
        )
    for pos_val in target_list[3:6]:
        assert_equal(
            pos_val, expected_three_vals[1],
        )
    for pos_val in target_list[6:9]:
        assert_equal(
            pos_val, expected_three_vals[2],
        )
    _check_target_prefix_const_are_three_vals(
        const_prefix=const_prefix,
        expected_three_vals=expected_three_vals)


def _check_target_prefix_const_are_three_vals(
        const_prefix, expected_three_vals):
    """
    対象のプリフィクスを持つ定数の各値が、3つずつの3つの値に
    なっていることを確認する。

    Parameters
    ----------
    const_prefix : str
        定数名のプリフィクス。
    expected_three_vals : list of int
        想定される3つの値のリスト。
    """
    count_dict = defaultdict(int)
    members = inspect.getmembers(observation_utils)
    for obj_name, obj_val in members:
        if not obj_name.startswith(const_prefix):
            continue
        if not isinstance(obj_val, int):
            continue
        count_dict[obj_val] += 1
    for expected_val in expected_three_vals:
        assert_equal(count_dict[expected_val], 3)


def _check_values_are_repeated_three_values(
        target_list, const_prefix, expected_three_vals):
    """
    対象の定数のリストの値が3つの値の繰り返し（例 : 1, 2, 3, 1, 2, 3, ...）
    になっていることと、指定のプリフィクスの定数の値が3つの想定値で
    3件ずつ存在することをチェックする。

    Parameters
    ----------
    target_list : list of int
        チェック対象の座標を格納した配列。
    const_prefix : str
        定数名のプリフィクス。
    expected_three_vals : list of int
        想定される3つの値のリスト。

    Raises
    ----------
    AssertionError
        - リスト内の値が3つの値の繰り返しになっていない場合。
        - 指定のプリフィクスを持つ定数が、想定値の3つの3つずつの
            値になっていない場合。
    """
    assert_equal(
        target_list[:3],
        expected_three_vals,
    )
    assert_equal(
        target_list[3:6],
        expected_three_vals,
    )
    assert_equal(
        target_list[6:9],
        expected_three_vals,
    )
    _check_target_prefix_const_are_three_vals(
        const_prefix=const_prefix,
        expected_three_vals=expected_three_vals)


def test_front_pos_const():
    _check_all_values_are_same(
        target_list=observation_utils.FRONT_POS_X_LIST,
        const_prefix='FRONT_POS_X_',
        expected_val=-151)
    _check_values_are_three_sequence(
        target_list=observation_utils.FRONT_POS_Y_LIST,
        const_prefix='FRONT_POS_Y_',
        expected_three_vals=[-100, 0, 100])
    _check_values_are_repeated_three_values(
        target_list=observation_utils.FRONT_POS_Z_LIST,
        const_prefix='FRONT_POS_Z_',
        expected_three_vals=[100, 0, -100])


def test_left_pos_const():
    _check_values_are_three_sequence(
        target_list=observation_utils.LEFT_POS_X_LIST,
        const_prefix='LEFT_POS_X_',
        expected_three_vals=[100, 0, -100])
    _check_all_values_are_same(
        target_list=observation_utils.LEFT_POS_Y_LIST,
        const_prefix='LEFT_POS_Y_',
        expected_val=-151)
    _check_values_are_repeated_three_values(
        target_list=observation_utils.LEFT_POS_Z_LIST,
        const_prefix='LEFT_POS_Z_',
        expected_three_vals=[100, 0, -100])


def test_right_pos_const():
    _check_values_are_three_sequence(
        target_list=observation_utils.RIGHT_POS_X_LIST,
        const_prefix='RIGHT_POS_X_',
        expected_three_vals=[-100, 0, 100])
    _check_all_values_are_same(
        target_list=observation_utils.RIGHT_POS_Y_LIST,
        const_prefix='RIGHT_POS_Y_',
        expected_val=151)
    _check_values_are_repeated_three_values(
        target_list=observation_utils.RIGHT_POS_Z_LIST,
        const_prefix='RIGHT_POS_Z_',
        expected_three_vals=[100, 0, -100])


def test_top_pos_const():
    _check_values_are_repeated_three_values(
        target_list=observation_utils.TOP_POS_X_LIST,
        const_prefix='TOP_POS_X_',
        expected_three_vals=[100, 0, -100])
    _check_values_are_three_sequence(
        target_list=observation_utils.TOP_POS_Y_LIST,
        const_prefix='TOP_POS_Y_',
        expected_three_vals=[-100, 0, 100])
    _check_all_values_are_same(
        target_list=observation_utils.TOP_POS_Z_LIST,
        const_prefix='TOP_POS_Z_',
        expected_val=151)


def test_back_pos_const():
    _check_all_values_are_same(
        target_list=observation_utils.BACK_POS_X_LIST,
        const_prefix='BACK_POS_X_',
        expected_val=151)
    _check_values_are_three_sequence(
        target_list=observation_utils.BACK_POS_Y_LIST,
        const_prefix='BACK_POS_Y_',
        expected_three_vals=[100, 0, -100])
    _check_values_are_repeated_three_values(
        target_list=observation_utils.BACK_POS_Z_LIST,
        const_prefix='BACK_POS_Z_',
        expected_three_vals=[100, 0, -100])


def test_bottom_post_const():
    _check_values_are_repeated_three_values(
        target_list=observation_utils.BOTTOM_POS_X_LIST,
        const_prefix='BOTTOM_POS_X_',
        expected_three_vals=[-100, 0, 100])
    _check_values_are_three_sequence(
        target_list=observation_utils.BOTTOM_POS_Y_LIST,
        const_prefix='BOTTOM_POS_Y_',
        expected_three_vals=[-100, 0, 100])
    _check_all_values_are_same(
        target_list=observation_utils.BOTTOM_POS_Z_LIST,
        const_prefix='BOTTOM_POS_Z_',
        expected_val=-151)


def _get_target_name_obj(obj_name, module_obj):
    """
    指定された名前のオブジェクトをobservation_utils.pyモジュールから
    取得する。

    Parameters
    ----------
    obj_name : str
        取得対象のオブジェクト名。
    module_obj : module
        検索対象のモジュール。

    Returns
    ----------
    obj_val : *
        取得されたオブジェクト。

    Raises
    ----------
    ValueError
        対象の名前のオブジェクトがモジュール内に存在しない場合。
    """
    members = inspect.getmembers(module_obj)
    for member_name, obj_val in members:
        if member_name != obj_name:
            continue
        return obj_val
    err_msg = \
        '指定された名前のオブジェクトがモジュール内に存在しません : %s'\
        % obj_name
    raise ValueError(err_msg)


def test_pos_and_pos_num_lists():
    const_type_str_key_lists_dict = {
        'FRONT': observation_utils.FRONT_POS_AND_POS_NUM_LISTS,
        'LEFT': observation_utils.LEFT_POS_AND_POS_NUM_LISTS,
        'RIGHT': observation_utils.RIGHT_POS_AND_POS_NUM_LISTS,
        'TOP': observation_utils.TOP_POS_AND_POS_NUM_LISTS,
        'BACK': observation_utils.BACK_POS_AND_POS_NUM_LISTS,
        'BOTTOM': observation_utils.BOTTOM_POS_AND_POS_NUM_LISTS,
    }
    for const_type_str, lists_obj in const_type_str_key_lists_dict.items():
        expected_pos_x_list = _get_target_name_obj(
            obj_name='%s_POS_X_LIST' % const_type_str,
            module_obj=observation_utils)
        expected_pos_y_list = _get_target_name_obj(
            obj_name='%s_POS_Y_LIST' % const_type_str,
            module_obj=observation_utils)
        expected_pos_z_list = _get_target_name_obj(
            obj_name='%s_POS_Z_LIST' % const_type_str,
            module_obj=observation_utils)
        expected_position_num_list = _get_target_name_obj(
            obj_name='POSITION_NUM_%s_LIST' % const_type_str,
            module_obj=observation_utils.sqlite_utils)

        assert_equal(
            lists_obj.pos_x_list,
            expected_pos_x_list,
        )
        assert_equal(
            lists_obj.pos_y_list,
            expected_pos_y_list,
        )
        assert_equal(
            lists_obj.pos_z_list,
            expected_pos_z_list,
        )
        assert_equal(
            lists_obj.position_num_list,
            expected_position_num_list,
        )


def test_get_position_num_by_world_location():

    # 定義が存在する組み合わせを指定した際の挙動を確認する。
    position_num = observation_utils.get_position_num_by_world_location(
        world_x=observation_utils.FRONT_POS_X_2_2,
        world_y=observation_utils.FRONT_POS_Y_2_2,
        world_z=observation_utils.FRONT_POS_Z_2_2)
    assert_equal(
        position_num,
        observation_utils.sqlite_utils.POSITION_NUM_FRONT_2_2,
    )
    position_num = observation_utils.get_position_num_by_world_location(
        world_x=observation_utils.BOTTOM_POS_X_3_3,
        world_y=observation_utils.BOTTOM_POS_Y_3_3,
        world_z=observation_utils.BOTTOM_POS_Z_3_3)
    assert_equal(
        position_num,
        observation_utils.sqlite_utils.POSITION_NUM_BOTTOM_3_3,
    )

    # 定義が存在しない座標を指定した際にエラーになることを確認する。
    kwargs = {
        'world_x': 100,
        'world_y': 100,
        'world_z': 100,
    }
    assert_raises(
        ValueError,
        observation_utils.get_position_num_by_world_location,
        **kwargs
    )
```

これで平面座標と位置の種別の取得処理などができるようになったので、オレンジ面の座標の取得結果のテストを加えます。シャッフルなどの実行前の初期位置でチェックが実行されるので、一通りオレンジ面が前面判定になることを確認します。

```py:Content\Scripts\action.py
    def test_get_orange_plane_world_locations(self):
        x_list, y_list, z_list = self.get_orange_plane_world_locations()
        for x, y, z in zip(x_list, y_list, z_list):
            _check_position_num_is_in_list(
                x=x, y=y, z=z,
                expected_position_num_list=sqlite_utils.\
                    POSITION_NUM_FRONT_LIST)


def _check_position_num_is_in_list(x, y, z, expected_position_num_list):
    """
    対象の座標の組み合わせによって算出される位置の種別値が、指定のリスト
    に含まれていることをチェックする。

    Parameters
    ----------
    x : int
        対象のワールド基準のX座標。
    y : int
        対象のワールド基準のY座標。
    z : int
        対象のワールド基準のZ座標。
    expected_position_num_list : list of int
        位置の種別値を格納している想定のリスト。

    Raises
    ----------
    AssertionError
        - もし算出された位置の種別値がリストに含まれていない場合。
    """
    position_num =  \
        observation_utils.get_position_num_by_world_location(
            world_x=x,
            world_y=y,
            world_z=z)
    is_in = position_num in expected_position_num_list
    assert_true(is_in)
```

実行してみて、テストで引っかからないことを確認し、他の色の平面に対してもBPの関数の追加 → action.pyに関数の追加 → テストの追加と進めておきます（割愛）。

# アクションの定数を共通モジュールに移動させる

PyActorで指定するモジュールだと、import上の問題がある（観測値関係などの他のモジュールから使いづらい）ため、action.pyに記載していた定数関係をcommon.action_utils.pyというパスでモジュールを用意してそちらに移動させておきます。

```py:Win64\common\action_utils.py
"""アクションに関連した共通の処理や定義などを記述したモジュール。
"""

ACTION_ROTATE_X_LEFT_1 = 1
ACTION_ROTATE_X_LEFT_2 = 2
ACTION_ROTATE_X_LEFT_3 = 3
ACTION_ROTATE_X_RIGHT_1 = 4
ACTION_ROTATE_X_RIGHT_2 = 5
ACTION_ROTATE_X_RIGHT_3 = 6
ACTION_ROTATE_Y_UP_1 = 7
ACTION_ROTATE_Y_UP_2 = 8
ACTION_ROTATE_Y_UP_3 = 9
ACTION_ROTATE_Y_DOWN_1 = 10
ACTION_ROTATE_Y_DOWN_2 = 11
ACTION_ROTATE_Y_DOWN_3 = 12
ACTION_ROTATE_Z_UP_1 = 13
ACTION_ROTATE_Z_UP_2 = 14
ACTION_ROTATE_Z_UP_3 = 15
ACTION_ROTATE_Z_DOWN_1 = 16
ACTION_ROTATE_Z_DOWN_2 = 17
ACTION_ROTATE_Z_DOWN_3 = 18
ACTION_RESET = 19

ACTION_LIST = [
    ACTION_ROTATE_X_LEFT_1,
    ACTION_ROTATE_X_LEFT_2,
    ACTION_ROTATE_X_LEFT_3,
    ACTION_ROTATE_X_RIGHT_1,
    ACTION_ROTATE_X_RIGHT_2,
    ACTION_ROTATE_X_RIGHT_3,
    ACTION_ROTATE_Y_UP_1,
    ACTION_ROTATE_Y_UP_2,
    ACTION_ROTATE_Y_UP_3,
    ACTION_ROTATE_Y_DOWN_1,
    ACTION_ROTATE_Y_DOWN_2,
    ACTION_ROTATE_Y_DOWN_3,
    ACTION_ROTATE_Z_UP_1,
    ACTION_ROTATE_Z_UP_2,
    ACTION_ROTATE_Z_UP_3,
    ACTION_ROTATE_Z_DOWN_1,
    ACTION_ROTATE_Z_DOWN_2,
    ACTION_ROTATE_Z_DOWN_3,
    ACTION_RESET,
]

ACTION_KEY_FUNC_NAME_DICT = {
    ACTION_ROTATE_X_LEFT_1: 'rotateXLeft1',
    ACTION_ROTATE_X_LEFT_2: 'rotateXLeft2',
    ACTION_ROTATE_X_LEFT_3: 'rotateXLeft3',
    ACTION_ROTATE_X_RIGHT_1: 'rotateXRight1',
    ACTION_ROTATE_X_RIGHT_2: 'rotateXRight2',
    ACTION_ROTATE_X_RIGHT_3: 'rotateXRight3',
    ACTION_ROTATE_Y_UP_1: 'rotateYUp1',
    ACTION_ROTATE_Y_UP_2: 'rotateYUp2',
    ACTION_ROTATE_Y_UP_3: 'rotateYUp3',
    ACTION_ROTATE_Y_DOWN_1: 'rotateYDown1',
    ACTION_ROTATE_Y_DOWN_2: 'rotateYDown2',
    ACTION_ROTATE_Y_DOWN_3: 'rotateYDown3',
    ACTION_ROTATE_Z_UP_1: 'rotateZUp1',
    ACTION_ROTATE_Z_UP_2: 'rotateZUp2',
    ACTION_ROTATE_Z_UP_3: 'rotateZUp3',
    ACTION_ROTATE_Z_DOWN_1: 'rotateZDown1',
    ACTION_ROTATE_Z_DOWN_2: 'rotateZDown2',
    ACTION_ROTATE_Z_DOWN_3: 'rotateZDown3',
    ACTION_RESET: 'reset',
}
```

# Observationの値を保存していく

用意したものを使って、観測値の保存処理を書いていきます。
まずは、アクションの処理が終わったタイミングをトリガーとして保存処理を走らせる必要があるため、action.pyを起点に処理を流します。

```py:Content\Scripts\action.py
class Action:
...
    def tick(self, delta_time):
        """
        ゲームPlay中、一定時間ごとに実行される関数。

        Parameters
        ----------
        delta_time : float
            前回のtick呼び出し後からの経過秒。
        """
        self._save_current_observation()
        self._set_ended_status_to_animation_ended_action()
        action_instruction_id, action, skip_animation = \
            self._get_action_instruction_data()
        if action_instruction_id is None:
            return
        action_func_name: str = self._get_action_func_name(
            action=action,
            skip_animation=skip_animation,
        )
        action_func = getattr(self.uobject, action_func_name)
        action_func()
        self._set_running_status(
            action_instruction_id=action_instruction_id)

    def _save_current_observation(self):
        """
        現在の観測値の保存を実行する。

        Notes
        ----------
        以下のケースでは保存は実行されない。
        - 回転のアニメーションなどが実行されている場合。
        - 新規で終了のステータスになったアクションが存在しない場合。

        Returns
        ----------
        saved : bool
            保存されたかどうか。
        """
        if self.is_any_cube_rotating():
            return False
        newly_ended_action_exists = False
        action_instructions = self.action_data_db_session.query(
            sqlite_utils.ActionInstruction
        ).filter_by(
            current_status=sqlite_utils.ACTION_INSTRUCTION_STATUS_RUNNING
        ).order_by(
            ActionInstruction.id.desc()
        ).limit(1)
        action_instruction: sqlite_utils.ActionInstruction
        for action_instruction in action_instructions:
            newly_ended_action_exists = True
            action_instruction_id = action_instruction.id
        if not newly_ended_action_exists:
            return
        observation_utils.save_observation(
            action_instruction_id=action_instruction_id,
            orange_plane_loc_tpl=self.get_orange_plane_world_locations(),
            white_plane_loc_tpl=self.get_white_plane_world_locations(),
            yellow_plane_loc_tpl=self.get_yellow_plane_world_locations(),
            green_plane_loc_tpl=self.get_green_plane_world_locations(),
            red_plane_loc_tpl=self.get_red_plane_world_location(),
            blue_plane_loc_tpl=self.get_blue_plane_world_location(),
        )
        return True
...
```

uobjectにアクセスしないと取れないデータ（各平面の座標や回転中のステータスなど）をaction.py側で取って、他の細かい保存処理などはobservation_utils.pyの共通モジュール側で処理します。

```py
...
def save_observation(
        action_instruction_id, orange_plane_loc_tpl, white_plane_loc_tpl,
        yellow_plane_loc_tpl, green_plane_loc_tpl, red_plane_loc_tpl,
        blue_plane_loc_tpl):
    """
    観測値のSQLiteへの保存処理を行う。

    Parameters
    ----------
    action_instruction_id : int
        直前のアクション設定の主キーのID。
    orange_plane_loc_tpl : tuple of lists
        オレンジの平面のワールド基準の座標のリストを格納したタプル。
    white_plane_loc_tpl : tuple of lists
        白色の平面のワールド基準の座標のリストを格納したタプル。
    yellow_plane_loc_tpl : tuple of lists
        黄色の平面のワールド基準の座標のリストを格納したタプル。
    green_plane_loc_tpl : tuple of lists
        緑色の平面のワールド基準の座標のリストを格納したタプル。
    red_plane_loc_tpl : tuple of lists
        赤色の平面のワールド基準の座標のリストを格納したタプル。
    blue_plane_loc_tpl : tuple of lists
        青色の平面のワールド基準の座標のリストを格納したタプル。

    Returns
    ----------
    observation_ids_dict : dict
        キーにカラータイプ、値に保存された観測値の主キーのIDの
        リストを格納する辞書。
    """
    observation_ids_dict = {}

    color_type_list = [
        sqlite_utils.COLOR_TYPE_ORANGE,
        sqlite_utils.COLOR_TYPE_WHITE,
        sqlite_utils.COLOR_TYPE_YELLOW,
        sqlite_utils.COLOR_TYPE_GREEN,
        sqlite_utils.COLOR_TYPE_RED,
        sqlite_utils.COLOR_TYPE_BLUE,
    ]
    plane_loc_tpl_list = [
        orange_plane_loc_tpl,
        white_plane_loc_tpl,
        yellow_plane_loc_tpl,
        green_plane_loc_tpl,
        red_plane_loc_tpl,
        blue_plane_loc_tpl,
    ]
    for color_type, plane_loc_tpl in zip(color_type_list, plane_loc_tpl_list):
        observation_ids = _save_each_plane_observation(
            action_instruction_id=action_instruction_id,
            plane_loc_tpl=plane_loc_tpl,
            color_type=color_type)
        observation_ids_dict[color_type] = observation_ids
    return observation_ids_dict


def _save_each_plane_observation(
        action_instruction_id, plane_loc_tpl, color_type):
    """
    対象の平面単体の観測値のSQLiteへの保存を行う。

    Parameters
    ----------
    action_instruction_id : int
        直前のアクション設定の主キーのID。
    plane_loc_tpl : tuple of lists
        対象の平面のワールド基準の座標のリストを格納したタプル。
        3件のインデックスで、X, Y, Zの順番でリストを格納する。
    color_type : int
        対象の位置の色の種別値。

    Returns
    ----------
    observation_id_list : list of int
        保存された観測値の主キーのIDのリスト。
    """
    session = sqlite_utils.create_action_data_db_session()
    x_list, y_list, z_list = plane_loc_tpl
    observation_id_list = []
    observation_list = []
    for x, y, z in zip(x_list, y_list, z_list):
        position_num = get_position_num_by_world_location(
            world_x=x,
            world_y=y,
            world_z=z)
        observation = sqlite_utils.Observation()
        observation.action_instruction_id = action_instruction_id
        observation.position_num = position_num
        observation.color_type = color_type
        session.add(instance=observation)
        observation_list.append(observation)
    session.commit()
    for observation in observation_list:
        observation_id_list.append(observation.id)
    return observation_id_list
```

テストもある程度書いておきます。

```py:Content\Scripts\action.py
    def test__save_current_observation(self):

        # 回転中の場合の挙動を確認する。
        original_is_any_cube_rotating_func = self.is_any_cube_rotating

        def dummy_is_any_cube_rotating():
            """
            テスト用の is_any_cube_rotating 関数のダミー関数。

            Returns
            ----------
            is_any_cube_rotating : bool
                固定でTrueが設定される。
            """
            return True

        self.is_any_cube_rotating = dummy_is_any_cube_rotating
        saved = self._save_current_observation()
        assert_false(saved)
        self.is_any_cube_rotating = original_is_any_cube_rotating_func

        # 新たに処理が終わっているアクションが存在しない場合の挙動を
        # 確認する。
        saved = self._save_current_observation()
        assert_false(saved)

        # 新たに観測値を保存する条件を満たすアクションが存在する場合の挙動を
        # 確認する。
        action_instruction = sqlite_utils.ActionInstruction()
        action_instruction.action = action_utils.ACTION_RESET
        action_instruction.current_status = \
            sqlite_utils.ACTION_INSTRUCTION_STATUS_RUNNING
        action_instruction.skip_animation = 0
        self.action_data_db_session.add(instance=action_instruction)
        self.action_data_db_session.commit()
        saved = self._save_current_observation()
        assert_true(saved)
        color_type_key_expected_position_nums_dict = {
            sqlite_utils.COLOR_TYPE_ORANGE:
            sqlite_utils.POSITION_NUM_FRONT_LIST,
            sqlite_utils.COLOR_TYPE_WHITE:
            sqlite_utils.POSITION_NUM_LEFT_LIST,
            sqlite_utils.COLOR_TYPE_YELLOW:
            sqlite_utils.POSITION_NUM_RIGHT_LIST,
            sqlite_utils.COLOR_TYPE_GREEN:
            sqlite_utils.POSITION_NUM_TOP_LIST,
            sqlite_utils.COLOR_TYPE_RED:
            sqlite_utils.POSITION_NUM_BACK_LIST,
            sqlite_utils.COLOR_TYPE_BLUE:
            sqlite_utils.POSITION_NUM_BOTTOM_LIST,
        }
        observation_list = []
        for color_type, expected_position_nums in \
                color_type_key_expected_position_nums_dict.items():
            for expected_position_num in expected_position_nums:
                # 保存が実行されており、値が取れる（エラーにならない）
                # ことを確認する。
                observation: sqlite_utils.Observation
                observation = self.action_data_db_session.query(
                    sqlite_utils.Observation
                ).filter_by(
                    action_instruction_id=action_instruction.id,
                    position_num=expected_position_num,
                    color_type=color_type,
                ).one()
                observation_list.append(observation)

        self.action_data_db_session.delete(instance=action_instruction)
        for observation in observation_list:
            self.action_data_db_session.delete(instance=observation)
        self.action_data_db_session.commit()
```

```py:Win64\common\tests\test_observation_utils.py
def test__save_each_plane_observation():
    observation_id_list =  observation_utils._save_each_plane_observation(
        action_instruction_id=3,
        plane_loc_tpl=(
            [
                observation_utils.FRONT_POS_X_2_2,
                observation_utils.BACK_POS_X_3_3,
            ], [
                observation_utils.FRONT_POS_Y_2_2,
                observation_utils.BACK_POS_Y_3_3,
            ], [
                observation_utils.FRONT_POS_Z_2_2,
                observation_utils.BACK_POS_Z_3_3,
            ]),
        color_type=sqlite_utils.COLOR_TYPE_RED)
    assert_equal(len(observation_id_list), 2)
    session = sqlite_utils.create_action_data_db_session()
    for i, observation_id in enumerate(observation_id_list):
        observation: sqlite_utils.Observation = session.query(
            sqlite_utils.Observation
        ).filter_by(
            id=observation_id
        ).one()
        assert_equal(observation.action_instruction_id, 3)
        if i == 0:
            assert_equal(
                observation.position_num,
                sqlite_utils.POSITION_NUM_FRONT_2_2,
            )
        if i == 1:
            assert_equal(
                observation.position_num,
                sqlite_utils.POSITION_NUM_BACK_3_3,
            )
        assert_equal(
            observation.color_type,
            sqlite_utils.COLOR_TYPE_RED)
        session.delete(observation)
    session.commit()


def test_save_observation():
    """save_observation 関数のテスト。
    """
    orange_plane_loc_tpl = (
        [
            observation_utils.FRONT_POS_X_1_1,
            observation_utils.FRONT_POS_X_2_1,
        ], [
            observation_utils.FRONT_POS_Y_1_1,
            observation_utils.FRONT_POS_Y_2_1,
        ], [
            observation_utils.FRONT_POS_Z_1_1,
            observation_utils.FRONT_POS_Z_2_1,
        ]
    )
    white_plane_loc_tpl = (
        [
            observation_utils.FRONT_POS_X_3_1,
            observation_utils.FRONT_POS_X_1_2,
        ], [
            observation_utils.FRONT_POS_Y_3_1,
            observation_utils.FRONT_POS_Y_1_2,
        ], [
            observation_utils.FRONT_POS_Z_3_1,
            observation_utils.FRONT_POS_Z_1_2,
        ]
    )
    yellow_plane_loc_tpl = (
        [
            observation_utils.FRONT_POS_X_2_2,
            observation_utils.FRONT_POS_X_3_2,
        ], [
            observation_utils.FRONT_POS_Y_2_2,
            observation_utils.FRONT_POS_Y_3_2,
        ], [
            observation_utils.FRONT_POS_Z_2_2,
            observation_utils.FRONT_POS_Z_3_2,
        ]
    )
    green_plane_loc_tpl = (
        [
            observation_utils.FRONT_POS_X_1_3,
            observation_utils.FRONT_POS_X_2_3,
        ], [
            observation_utils.FRONT_POS_Y_1_3,
            observation_utils.FRONT_POS_Y_2_3,
        ], [
            observation_utils.FRONT_POS_Z_1_3,
            observation_utils.FRONT_POS_Z_2_3,
        ]
    )
    red_plane_loc_tpl = (
        [
            observation_utils.FRONT_POS_X_3_3,
            observation_utils.LEFT_POS_X_1_1,
        ], [
            observation_utils.FRONT_POS_Y_3_3,
            observation_utils.LEFT_POS_Y_1_1,
        ], [
            observation_utils.FRONT_POS_Z_3_3,
            observation_utils.LEFT_POS_Z_1_1,
        ]
    )
    blue_plane_loc_tpl = (
        [
            observation_utils.LEFT_POS_X_2_1,
            observation_utils.LEFT_POS_X_3_1,
        ], [
            observation_utils.LEFT_POS_Y_2_1,
            observation_utils.LEFT_POS_Y_3_1,
        ], [
            observation_utils.LEFT_POS_Z_2_1,
            observation_utils.LEFT_POS_Z_3_1,
        ]
    )
    observation_ids_dict = observation_utils.save_observation(
        action_instruction_id=2,
        orange_plane_loc_tpl=orange_plane_loc_tpl,
        white_plane_loc_tpl=white_plane_loc_tpl,
        yellow_plane_loc_tpl=yellow_plane_loc_tpl,
        green_plane_loc_tpl=green_plane_loc_tpl,
        red_plane_loc_tpl=red_plane_loc_tpl,
        blue_plane_loc_tpl=blue_plane_loc_tpl)
    assert_equal(len(observation_ids_dict), 6)
    for color_type, observation_ids in observation_ids_dict.items():
        assert_true(isinstance(observation_ids, list))
        is_in = color_type in sqlite_utils.COLOR_TYPE_LIST
        assert_true(is_in)
        assert_equal(len(observation_ids), 2)

    # 保存された観測値の位置の番号が想定した値になっているかを確認する。
    color_type_key_expected_position_nums_dict = {
        sqlite_utils.COLOR_TYPE_ORANGE: [
            sqlite_utils.POSITION_NUM_FRONT_1_1,
            sqlite_utils.POSITION_NUM_FRONT_2_1,
        ],
        sqlite_utils.COLOR_TYPE_WHITE: [
            sqlite_utils.POSITION_NUM_FRONT_3_1,
            sqlite_utils.POSITION_NUM_FRONT_1_2,
        ],
        sqlite_utils.COLOR_TYPE_YELLOW: [
            sqlite_utils.POSITION_NUM_FRONT_2_2,
            sqlite_utils.POSITION_NUM_FRONT_3_2,
        ],
        sqlite_utils.COLOR_TYPE_GREEN: [
            sqlite_utils.POSITION_NUM_FRONT_1_3,
            sqlite_utils.POSITION_NUM_FRONT_2_3,
        ],
        sqlite_utils.COLOR_TYPE_RED: [
            sqlite_utils.POSITION_NUM_FRONT_3_3,
            sqlite_utils.POSITION_NUM_LEFT_1_1,
        ],
        sqlite_utils.COLOR_TYPE_BLUE: [
            sqlite_utils.POSITION_NUM_LEFT_2_1,
            sqlite_utils.POSITION_NUM_LEFT_3_1,
        ]
    }
    session = sqlite_utils.create_action_data_db_session()
    for color_type, expected_position_nums in \
            color_type_key_expected_position_nums_dict.items():
        observation_ids = observation_ids_dict[color_type]
        for i, observation_id in enumerate(observation_ids):
            expected_position_num = expected_position_nums[i]
            observation: sqlite_utils.Observation = session.query(
                sqlite_utils.Observation
            ).filter_by(
                id=observation_id
            ).one()
            assert_equal(observation.position_num, expected_position_num)
```

Playボタンを押して試してみます。
SQLiteに値を手作業で保存してみて動かしてみます。
action=1, current_status=1で保存しました。眺めていると一度回転のアニメーションが終わり、current_status=3となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/f35a2ea1-21c1-f345-86ad-796d96f528da.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/e3ac533b-0565-40ba-c66c-20437a0cfa8c.png)

observationテーブルの値も確認してみます・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/ba15879f-78d6-83de-4f41-c21bff19b275.png)

ちゃんと保存されているようです。
position_numとかもソートしてみて、重複がないかなども軽く確認してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/690781ec-d592-eb76-90aa-1f3fbe482a9c.png)

とりあえずは大丈夫そうですかね・・・

# リセットしたらリセットより前のデータを削除するようにしておく

リセットのアクションをしたら、それよりも前のSQLiteのアクションの指示のデータと観測値のデータを削除するようにしておきます。

```py:Content\Scripts\action.py
...
    def tick(self, delta_time):
        """
        ゲームPlay中、一定時間ごとに実行される関数。

        Parameters
        ----------
        delta_time : float
            前回のtick呼び出し後からの経過秒。
        """
        self._save_current_observation()
        updated_action_instructions = \
            self._set_ended_status_to_animation_ended_action()
        self._del_old_action_data_if_reset_action_exists(
            updated_action_instructions=updated_action_instructions)
...
    def _del_old_action_data_if_reset_action_exists(
            self, updated_action_instructions):
        """
        完了したアクションの中に、リセットのアクションが含まれている
        場合に、リセット前までの古いデータの削除を行う。

        Parameters
        ----------
        updated_action_instructions : list of ActionInstruction
            完了したアクションの設定のリスト。

        Returns
        ----------
        deleted : bool
            削除処理が実行された場合にTrueが設定される。
        """
        if not updated_action_instructions:
            return False
        updated_action_instruction: sqlite_utils.ActionInstruction
        reset_action_id = None
        for updated_action_instruction in updated_action_instructions:
            if updated_action_instruction.action != action_utils.ACTION_RESET:
                continue
            reset_action_id = updated_action_instruction.id
        if reset_action_id is None:
            return False
        self.action_data_db_session.query(
            sqlite_utils.ActionInstruction
        ).filter(
            sqlite_utils.ActionInstruction.id < reset_action_id
        ).delete()
        self.action_data_db_session.query(
            sqlite_utils.Observation
        ).filter(
            sqlite_utils.Observation.action_instruction_id < reset_action_id
        ).delete()
        self.action_data_db_session.commit()
        return True
...
    def _del_old_data_if_reset_action_exists(
            self, updated_action_instructions):
        """
        完了したアクションの中に、リセットのアクションが含まれている
        場合に、リセット前までの古いデータの削除を行う。

        Parameters
        ----------
        updated_action_instructions : list of ActionInstruction
            完了したアクションの設定のリスト。

        Returns
        ----------
        deleted : bool
            削除処理が実行された場合にTrueが設定される。
        """
        if not updated_action_instructions:
            return False
        updated_action_instruction: sqlite_utils.ActionInstruction
        reset_action_id = None
        for updated_action_instruction in updated_action_instructions:
            if updated_action_instruction.action != action_utils.ACTION_RESET:
                continue
            reset_action_id = updated_action_instruction.id
        if reset_action_id is None:
            return False
        self.action_data_db_session.query(
            sqlite_utils.ActionInstruction
        ).filter(
            sqlite_utils.ActionInstruction.id < reset_action_id
        ).delete()
        self.action_data_db_session.query(
            sqlite_utils.Observation
        ).filter(
            sqlite_utils.Observation.action_instruction_id < reset_action_id
        ).delete()
        self.action_data_db_session.commit()
        return True
```

アクションが完了したものに対して完了のステータスを設定する処理（_set_ended_status_to_animation_ended_action）の返却値で、更新されたアクションのデータのリストを返すように調整してあります。
そちらのリストを参照して、もしリセットのアクションのデータがある場合は削除処理を実行する関数を実行しています。

簡易のテストも追加しておきます。

```py:Scripts\action.py
    def test__del_old_data_if_reset_action_exists(self):
        # 空のリストを指定した際に処理が止まることを確認する。
        deleted = self._del_old_data_if_reset_action_exists(
            updated_action_instructions=[])
        assert_false(deleted)

        # リセットのアクションがリストに含まれない場合に処理が止まる
        # ことを確認する。
        action_instruction_normal_rot = sqlite_utils.ActionInstruction()
        action_instruction_normal_rot.action = \
            action_utils.ACTION_ROTATE_X_LEFT_1
        action_instruction_normal_rot.current_status = \
            sqlite_utils.ACTION_INSTRUCTION_STATUS_ENDED
        action_instruction_normal_rot.skip_animation = 0
        self.action_data_db_session.add(
            instance=action_instruction_normal_rot)
        self.action_data_db_session.commit()
        updated = self._del_old_data_if_reset_action_exists(
            updated_action_instructions=[action_instruction_normal_rot])
        assert_false(updated)

        # リセットのアクションを含む場合に、リセット前のデータが削除
        # されることを確認する。
        action_instruction_reset = sqlite_utils.ActionInstruction()
        action_instruction_reset.action = action_utils.ACTION_RESET
        action_instruction_reset.current_status = \
            sqlite_utils.ACTION_INSTRUCTION_STATUS_ENDED
        action_instruction_reset.skip_animation = 0
        self.action_data_db_session.add(action_instruction_reset)

        observation_normal_rot = sqlite_utils.Observation()
        observation_normal_rot.action_instruction_id = \
            action_instruction_normal_rot.id
        observation_normal_rot.position_num = \
            sqlite_utils.POSITION_NUM_FRONT_1_1
        observation_normal_rot.color_type = \
            sqlite_utils.COLOR_TYPE_ORANGE
        self.action_data_db_session.add(observation_normal_rot)

        observation_reset = sqlite_utils.Observation()
        observation_reset.action_instruction_id = action_instruction_reset.id
        observation_reset.position_num = sqlite_utils.POSITION_NUM_FRONT_1_1
        observation_reset.color_type = sqlite_utils.COLOR_TYPE_ORANGE
        self.action_data_db_session.add(observation_reset)

        self.action_data_db_session.commit()
        updated = self._del_old_data_if_reset_action_exists(
            updated_action_instructions=[
                action_instruction_normal_rot,
                action_instruction_reset,
            ])
        assert_true(updated)

        # リセット以降のアクションデータのみ残っていることを確認する。
        min_action_instruction = self.action_data_db_session.query(
            func.min(sqlite_utils.ActionInstruction.id).label('min_id')
        ).one()
        assert_equal(
            min_action_instruction.min_id,
            action_instruction_reset.id)

        # リセット以降の観測値のデータのみ残っていることを確認する。
        min_observation = self.action_data_db_session.query(
            func.min(sqlite_utils.Observation.id).label('min_id')
        ).one()
        assert_equal(
            min_observation.min_id,
            observation_reset.id)

        self.action_data_db_session.delete(action_instruction_reset)
        self.action_data_db_session.delete(observation_reset)
        self.action_data_db_session.commit()
```

手動でSQLiteに値を入れてみて、実際に動作を確認してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/f501da21-6c9f-3395-462f-0aecaf6b6188.png)

2つアクションの指示（2回転分）のデータを入れました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/32b48896-158e-6b2a-2e15-fdf3a19e787e.png)

観測値もそれぞれのアクションの値が入っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/c9b299d2-53e1-9e69-28e4-ed608df52444.png)

リセットのアクション（action=19）を入れてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/c86f9561-c368-774d-6e0c-313ba4b04fbb.png)

キューブの回転がシャッフルされます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/d93a35b8-d4f9-8d5a-4c27-43a6f5061bc2.png)

アクションの値を確認すると、正しくリセットのもののみになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/1b404d08-cb9a-b9a2-48ef-72476fa0cf38.png)

観測値の方もリセットのアクションのデータのみになっています。大丈夫そうですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/31f37f2c-cc15-62dc-a9fb-d19c91cdd632.png)

# リワード回りの対応は・・・

リワード関係は観測値が取得できれば後は如何様にもできる気がするため、
実装はUE4プロジェクト内では行わずにPyPI（pip）登録用のライブラリ側で対応します。

これは、Pythonスクリプトで細かく調整が必要になった際にいちいちUE4でリリース用にパッケージして・・・となるとしんどそうなためです（Pythonで完結するライブラリ側のアップデートのみで対応できるようにしておきたい）。

ということで一旦後回しにします。

# ゲーム終了のアクションを追加しておく

何かとテストするときなども便利な気がするため、アクションの指定でゲーム終了（Playの終了）ができるようにしておきます。

検索してみたら、Quit GameというBPのノードが存在するようです。

参考 : [Quit Game](https://docs.unrealengine.com/en-US/BlueprintAPI/Game/QuitGame/index.html)

BP_ActionにquitGameという関数を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/2b76304b-76cf-fe2a-711e-8ebfa9d740b5.png)

Python側で、ACTION_QUIT_GAMEという定数や関数名とのマッピングの定義などを追加していきます。

```py:Win64\common\action_utils.py
...
ACTION_ROTATE_Z_DOWN_3 = 18
ACTION_RESET = 19
ACTION_QUIT_GAME = 20
...
ACTION_LIST = [
   ...
    ACTION_ROTATE_Z_DOWN_3,
    ACTION_RESET,
    ACTION_QUIT_GAME,
]

ACTION_KEY_FUNC_NAME_DICT = {
    ...
    ACTION_ROTATE_Z_DOWN_3: 'rotateZDown3',
    ACTION_RESET: 'reset',
    ACTION_QUIT_GAME: 'quitGame',
}
```

SQLiteに値を入れて試してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/ece686d1-0181-983e-1779-87a15f183cc4.png)

Play画面を見ていると、ちゃんとPlayしていない状態になりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/9015ebe9-c3be-bcbb-d430-2ba83fa71c94.png)

# 余分なアセットを削除する

段々リリース用のパッケージングが近くなってきました。
不慣れなこともありとりあえずStarter Content有りでUE4プロジェクトを作っているので、現状余分なアセットが色々プロジェクトに入っています。

パッケージで余分にサイズがかさんでしまったり、ビルド時間が長くなりそうな気がするため、削除しておきます。

また、その時の名残りでMapsをStarter Content内のもので作業してしまっていたので、そちらのマップをContent直下にMapsというフォルダを作って移動させておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/e78521a4-660d-91e8-3ef1-530bb34903bc.png)

Shapesフォルダだけ残しました。
中には以下のようにCubeとPlaneが入っています。これ消してもBasicのメニューにあるので問題なさそう・・・も気もしますが、一応残しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/b40dc087-a80e-34cd-dac6-3657e999a152.png)

Starter Content内のマップも移動させて、名前をDefault Mapとしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/3b2a82b6-389d-b4fe-198d-e4238eba3a70.png)

ただ、これだとUE4を再起動した際にマップが表示されません（マップのコンテンツをダブルクリックすると開いてくれる）。

デフォルトのレベルの指定を切り替える必要がありそうなので、対応します。

参考 : [デフォルトのレベルを変更する](https://docs.unrealengine.com/ja/Engine/Levels/HowTo/ChangeDefaultLevel/index.html)

プロジェクト設定を開いて、Maps & Modesのメニューを表示し、Game Default Mapを編集します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/2d890e16-f1a1-ee4f-866c-0aeb98989464.png)

余分なコンテンツを削除したので、今は一つしか選択肢がありません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/02bd6799-375e-5b7c-c679-809cfb7b85fb.png)

同様にDditor Startup Mapの方も切り替えておきます（UE4再起動時にはこちらも調整しないと最初にレベルを開いてくれなさそうな印象です）。

設定後、一応UE4を再起動してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/51484c56-7649-2df0-3770-15912cacdd4b.png)

ちゃんと想定していたレベルが開かれているようです。Playしてみてエラーやテストで引っかからないことを確認してから、削除分や移動分などをコミットします。

# パッケージングを試してみる

UE4のパッケージングというか、リリースビルド的なところを試したことがないので、試していきます。

とりあえずPackagingメニューでWindowsの64bitを選択してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/5b7a6074-f8fe-5a4b-d07e-6801f21dc29d.png)

Visual Studio Communityインストールしてね、というメッセージが出たのでインストールしてきます。
リンクをクリックしたところ、一気にインストーラーのダウンロードになりました。迷わなくていいですね。

ただ、そちらのインストーラー経由でインストールしてみたところ、Community 2015という表示が・・・
メッセージでは2017と書かれていますが、大丈夫なんだろうか・・・と不安になりますが、とりあえずそのまま進めてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/2a9cdeb1-aef4-91f9-97bd-0b521c1a81a0.png)

インストールに結構時間がかかるのでしばらくお茶でも飲みながらのんびりします。

インストールが終わったのでもう一度Win64でのパッケージングを試してみます。

```
UATHelper: Packaging (Windows (64-bit)): BUILD SUCCESSFUL
UATHelper: Packaging (Windows (64-bit)): AutomationTool exiting with ExitCode=0 (Success)
```

1分かかったか？というレベルでさくっと終わりました。
パッケージングされたexeファイルを起動してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/86e623ed-3002-339d-e630-149cd06d69a3.png)

何やらエラーが。
色々調べたものの、情報が少なめで結構苦戦しています。

よくよくプラグインのREADMEを読んでみると、

> Binary releases are mainly useful for editor scripting, if you want to package your project for distribution and you need the python runtime, you need a source release (see below).
> https://github.com/20tab/UnrealEnginePython#binary-installation-on-windows-64-bit

という記述が。
ああ、Binary releasesのPythonだと、配布用パッケージングで使えないのか・・・
他の方のissueでも、それで解決したよ、という記述が。

> Packaged Application Crashes #791
> ...
> Hello, the Python an UE4 integration works fine in the editor. When I package the project and run it, I get the following error:
> ...
> Source installation of the same Python version resolved this issue.
> https://github.com/20tab/UnrealEnginePython/issues/791

完全に見落としていました・・・
面倒ですが、しょうがないので「Installation from sources on Windows (64 bit)」の部分のドキュメントを読みつつ進めてみます。

とりあえず以下のリポジトリのクローンを実施しました。

```
https://github.com/20tab/UnrealEnginePython
```

その後、プロジェクトフォルダ直下のPluginsフォルダにこのUnrealEnginePythonフォルダを入れます（こちらのものは、既存のバイナリ版によるBinariesとConfigフォルダが無いので、後で削除が必要かもしれません。でもそうするとNumPyとかのインストールどうするんだろう・・）。

> - from the file explorer right click on the project main file and choose 'generate visual studio project files'
- open visual studio, you should now see Plugins/UnrealEnginePython in your solution explorer
- run the compilation from visual studio
> https://github.com/20tab/UnrealEnginePython#installation-from-sources-on-windows-64-bit

ここが少々分かりません。Visual Studioほとんど使ったことがないというのもありますが・・・
ファイルエクスプローラーは普通のエクスプローラー？特に→クリックで`generate visual studio project files`といったメニューは無い感じですね・・・

ただ、UE4Editor上でも対応できる？っぽいのでそちらで進めてみます。
プラグイン画面を開いて、Package..をクリックしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/f56fb2cb-f7ce-9063-4288-56e89d29ae57.png)

```
UATHelper: Package Plugin Task (Windows):   ERROR: Unable to instantiate module 'UnrealEnginePython': System.Exception: Unable to find Python installation
```

怒られました。
Pythonが見つかりませんと。

> Currently python3.6, python3.5 and python2.7 are supported. 

Python3.6か3.5が必要らしい。
以下のビルド用のC#のコードで指定されているとこにPython3.6とかが必要なようです。

https://github.com/20tab/UnrealEnginePython/blob/fbc97f27861aac456022756c9fdaf7e3a3fe33c1/Source/UnrealEnginePython/UnrealEnginePython.Build.cs#L19

以下のPython公式のページの、「Windows x86-64 executable installer」をダウンロードしてインストールしてみます。
https://www.python.org/downloads/release/python-361/

インストール後、そこのパスをBuild.csのwindowsKnownPathsに追記しておきます。

```c#
    private string[] windowsKnownPaths =
    {
       // "C:/Program Files/Python37",
        "C:/Program Files/Python36",
        "C:/Program Files/Python35",
        "C:/Python27",
        "C:/IntelPython35",
        "C:/Users/*******/AppData/Local/Programs/Python/Python36",
    };
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/e464e1c0-0d48-dbce-c132-bf1a3fa9c1d2.png)

今度はエラーにならずにちゃんとプラグインのパッケージングが進んでいってくれました。
が、今度は途中で別のエラーが。

```
ERROR: UBT ERROR: Failed to produce item: ...\Plugins\UnrealEnginePython\Binaries\Win64\UE4Editor-PythonAutomation.pdb
```

Binaries以下へのファイルの生成で失敗しているようです。これは・・・既存のBinariesのファイルが悪さをしているのだろうか。
後でGitやQiitaの記述から元に戻せばいいか・・ということで、試しにBinariesフォルダを消してしまいます（事前にUE4は一旦落としておきます）。

削除後にUE4を起動すると、プラグインでビルドがされていないものがあるよ、と出てビルドする流れになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/016d7402-b210-4a4c-3e50-b6ea1e61e00e.png)

ビルドが終わるまでしばらく待ちます。

ビルド完了後、なんとか動きだしてくれました（NumPyなどがimport効かないという点はさておき）。
が、パッケージングしてみるとexeファイルでまた同様のエラーになります。

他にもいろいろ試しましたが、うまくいかず。
うーん・・・これは、Pythonプラグインをパッケージングするの、大分厳しいですね・・・
（同様のエラーでクローズしていないissueも目に付く点も含め・・・）

パッケージング用のビルド、もっと初期のころに確認取っておかないと駄目ですね・・・（自戒）

しばらく粘ったのですが、厳しそうなので、将来的にはPythonプラグインをUE4プロジェクトで使わない形にし、一旦はここまでで観測値はUE Editorでダイレクトに取れるので、そちらで個人的に試したりは使っていこうと思います。

# 参考ページまとめ

- [Quit Game](https://docs.unrealengine.com/en-US/BlueprintAPI/Game/QuitGame/index.html)
- [デフォルトのレベルを変更する](https://docs.unrealengine.com/ja/Engine/Levels/HowTo/ChangeDefaultLevel/index.html)
- [UnrealEnginePython](https://github.com/20tab/UnrealEnginePython#binary-installation-on-windows-64-bit)
- [Packaged Application Crashes #791](https://github.com/20tab/UnrealEnginePython/issues/791)