機械学習周りのプログラミング中心。 イベント情報
ポケモンバトルAI本電子書籍通販中

【CodinGameオセロ】ソースコードの分割と自動連結

前回までで、評価関数を開発しない範囲でランダムより強いAIを実装してきました。機械学習に入る前に一点だけ改良を加えておきます。それはソースコードの分割です。ソースコードが1000行に達しており、1ファイルに記述し続けるのは見通しが悪い状態になってきています。もともと1ファイルにしていた理由は、CodinGameに投稿できるのが単一のソースファイルに限られているためです。この制約を考慮しつつ、ソースファイルの分割に着手します。

今回のソースコードは以下のバージョンです。

https://github.com/select766/codingame-othello/tree/d7736ae592aa856a96c1ac7f8572e377420b4063

分割の方針は以下のようにしました。

  • クラス単位でヘッダ(*.hpp)ファイルに分ける。
    • 盤面クラスや探索部はヘッダファイルに入れる。
  • 機能ごとのmain関数を別々のソース(*.cpp)ファイルに分ける。
    • 機能: CodinGame投稿用、自己対戦用、合法手テストケース生成用など
  • Makefileによって各機能のバイナリを生成する。
    • ifdefにより機能ごとのビルドを行う機能は廃止。

ファイルは以下のように分割されました。(一部省略)

board.hpp 盤面クラス
search_base.hpp 探索部の基底クラス
search_random.hpp ランダムAI
search_alpha_beta_iterative.hpp アルファベータ法AI
common.hpp main_*.cppからインクルードする単一のヘッダ。ここから他のhppファイルをインクルードする。
main_codingame.cpp CodinGameで使用するmain関数
main_random_match.cpp 自己対戦用main関数

ここで、CodinGame投稿用のコードは、単一ファイルにまとめる機能が必要になります。CodinGameの投稿に必要なソースコードは、main_codingame.cppと、そこからインクルードされているヘッダファイルです。#includeは、その文をインクルードされているファイルで置換することができ、C++コンパイル時のプリプロセッサではそのような機能が実装されています。ただし、本物のプリプロセッサを用いると#include <memory>のような標準ライブラリまで連結してしまいますので、Pythonコードで文字列処理を用いて連結するツールを独自に実装しました。このツールでは、#include "board.hpp"のようにファイル名がダブルクオーテーションで囲まれている場合のみインクルードの処理を行います。また、依存関係の都合で同じファイルが2回インクルードされる可能性がありますが、出力されるソースコードの長さを短くするため最初の1回だけ出力するようにヒューリスティックな実装を加えています。

ビルド用のMakefileを以下のように作りました。Python製のツールconcat_source.pyを用いてソースコードを提出用に一つにまとめます。さらに、それに文法エラーがないことを確認するためローカルでのビルドも行います。

CFLAGS = -std=c++17 -O3

SRCDIR = src
OUTDIR = build
HEADERS = $(wildcard $(SRCDIR)/*.hpp)
SRCS = $(wildcard $(SRCDIR)/*.cpp)
OBJS = $(SRCS:.cpp=.o)

.PHONY: all clean

all: $(OUTDIR)/codingame $(OUTDIR)/random_match
clean:
    rm -rf $(OUTDIR)/* $(SRCDIR)/*.o

.cpp.o:
    g++ -c $(CFLAGS) -o $@ $<

# 提出用ファイルの生成
$(OUTDIR)/codingame.cpp: src/main_codingame.cpp $(HEADERS)
    mkdir -p $(@D)
    python concat_source.py -o $@ src main_codingame.cpp

# 提出用ファイルに文法エラーがないか検証するためローカルでもコンパイル
$(OUTDIR)/codingame: $(OUTDIR)/codingame.o
    mkdir -p $(@D)
    g++ -o $@ $^ $(CFLAGS)

# 自己対戦機能のコンパイル
$(OUTDIR)/random_match: $(SRCDIR)/main_random_match.o
    mkdir -p $(@D)
    g++ -o $@ $^ $(CFLAGS)

以上の作業により、今後機能が増加してもソースファイルを分割できるようになりました。