BASIC (ベーシック) であそぼう 8
〜 オセロで対戦 〜








2022-12-22 作成 福島
TOP > asob > decbasic-othello
[ TIPS | TOYS | OTAKU | LINK | MOVIE | CGI | AvTitle | ConfuTerm | HIST | AnSt | Asob ]

オセロという名のボードゲーム

ボードゲームは種類がたくさんありますが、ルールの規模が比較的小さいものにオセロ*1が挙げられます。
*1ゲーム名について諸説ありますが、知名度の高さで「オセロ」とそのルール*2を採用しています。
  検索結果数 : Reversi game(約 1,470,000 件) / Othello game(約 4,860,000 件)
*2石の初期配置をクロス、色は黒白の 2 色、先手は黒番とします。


前稿の会話プログラムを使って、オセロの対戦ゲームを作ります。
近年ではコンピュータでの対戦オセロというと、相手がコンピュータのこともありますが、ここで扱うのは人間同士の対戦です。

プログラムの簡便さを優先させているため、自動進行*3の機能はありません。相手石の読み込みやパスは手動で実施してください。
*3相手の打った石を自動的に読み込んだり、パスをする機能のこと。


0. 事前準備

0-1. この章では、十進 BASIC を使います。
インストールがまだなら、ここ (ラズパイ400)ここ (CentOS) の gtk2 版か、
ここ (Windows10) を参考にしてインストールしておいてください。
0-2. この章では、別のテキストエディタを使いません。
十進 BASIC には専用のエディタ画面があるので、これを使います。
0-3. 未経験者向けの情報を省いています。
プログラミング未経験の方は、予めこちら (BASIC であそぼう 1) を学習しておくことを推奨します。
0-4. ひとつの PC で 十進 BASIC を複数起動します。
狭い画面でも実行できますが、一度に複数のプログラムを起動するので、見易くするためには広い画面を用意してください。
0-5. 制限版のプログラムになっています。
下記プログラムファイル comm.BAS では、こちらの排他ロックプログラム exLock.BAS を使用します。
この排他ロックプログラムには Windows 専用版の exLockWin.BAS もあります。
違いは、自動解除機能の有無です。


1. 前回 BASIC であそぼう 7 - 第 2 項 のおさらい

1-1. 複数で会話するプログラム
<テキスト21> では以下の様に記述していました。
これを変更していきます。

<テキスト21> ファイル名「user2.BAS」
DECLARE EXTERNAL SUB COMM.Init
DECLARE EXTERNAL SUB COMM.Quiet
DECLARE EXTERNAL SUB COMM.Talk

CALL COMM.Init("user2", "R:\")     ! 自分の識別名と伝言台を指定。
CALL COMM.Quiet

DO
    ! メッセージ送信の入力。
    ! 送りたいメッセージがなければ受信だけを行う。
    INPUT PROMPT "誰へ?": to$
    IF to$ = "." THEN       ! プログラムを終了するときは「.」を入力する。
        PRINT "終了します。"
    ELSEIF to$ <> "" THEN
        INPUT PROMPT "内容?": msg$
    END IF

    CALL COMM.Talk(msgFrom$, to$, msg$)
    SELECT CASE COMM.Result
        CASE -1     ! 発言なし。
            PRINT "問題なし。"
        CASE  0
            PRINT "発言失敗。"
        CASE  1
            PRINT "発言成功。"
        CASE  2     ! 発言の前にメッセージが到着していた。
            PRINT "メッセージ受信。From:" & msgFrom$ & " " & msg$
            IF to$ <> "" THEN
                PRINT "発言は失敗。"
            END IF
    END SELECT
LOOP

END


MERGE "comm.BAS"


2. 対戦オセロプログラム

2-1. オセロプログラムの作成
<テキスト22> ファイル名「black.BAS」(黒番=先手)
上記 <テキスト21> を変更して作成する。
LET myColor = 1  ! 自分の石の色*4。(1:黒 / 2:白)
LET revColor = 1 + 1 - (myColor - 1) ! 相手の色 (= 自分の反対色) を求める。

! 自分と相手の識別名を用意する。
DIM commId$(2)
LET commId$(1) = "black"
LET commId$(2) = "white"

DECLARE EXTERNAL SUB COMM.Init
DECLARE EXTERNAL SUB COMM.Quiet
DECLARE EXTERNAL SUB COMM.Talk

CALL COMM.Init(commId$(myColor), "R:\")   ! 自分の識別名と伝言台を指定。
CALL COMM.Quiet

DECLARE EXTERNAL SUB SetBoard
DECLARE EXTERNAL SUB ListLegalPlaces
DECLARE EXTERNAL FUNCTION LengthLegalPlaces
DECLARE EXTERNAL SUB BoardDisplay
DECLARE EXTERNAL FUNCTION MsgToPos
DECLARE EXTERNAL FUNCTION IsLegalPlace
DECLARE EXTERNAL FUNCTION GetDirLength
DECLARE EXTERNAL SUB RevDirLength
DECLARE EXTERNAL FUNCTION CountingStone

! 盤面を用意する。
DIM board(8,8)
CALL SetBoard(board)

! 石を置ける場所のリスト。(y * 10 + MOD(x,10))
DIM legalPlaces(2, 8 * 8 - 4 + 1)  ! 1:黒番のリスト 2:白番のリスト。

DO
    ! 石を置ける場所を探す。
    CALL ListLegalPlaces(legalPlaces, 1, board) ! 黒番の分。
    CALL ListLegalPlaces(legalPlaces, 2, board) ! 白番の分。

    ! 石を置ける場所の数を求める。
    LET myAvailLength  = LengthLegalPlaces(legalPlaces, myColor)     ! 自分の設置可能数。
    LET revAvailLength = LengthLegalPlaces(legalPlaces, revColor)    ! 相手の設置可能数。

    ! 盤面を表示する。
    CALL BoardDisplay(board, legalPlaces, myColor)

    IF myAvailLength = 0 THEN
        IF revAvailLength = 0 THEN
            PRINT "双方共に石を置く場所がありません。終局します。"
            EXIT DO
        ELSE
            PRINT "石を置く場所がありません。パスしてください。"
        END IF
    END IF

    ! メッセージ送信の入力。
    ! 送りたいメッセージがなければ受信だけを行う。
    LET to$ = ""
    INPUT PROMPT "内容?" & "-> " & commId$(revColor) & " ": msg$
    IF msg$ = "." THEN
        PRINT "終了します。"
        EXIT DO
    END IF

    LET myDL = 0
    IF msg$ <> "" THEN
        LET mPos = MsgToPos(msg$)    ! 位置指定文字を数値に変換する。
        LET myPx = MOD(mPos,10)
        LET myPy = INT(mPos/10)
        IF myPx = 0 OR myPy = 0 THEN
            PRINT "置く石の位置は a1~h8 で指定してください。"
        ELSEIF IsLegalPlace(legalPlaces, myColor, myPy, myPx) = 0 THEN
            PRINT "石を置けません。"
        ELSE
            LET to$ = commId$(revColor)
            ! 裏返せる相手石数を調査する。
            LET myDL = GetDirLength(board, myPy, myPx, myColor)
        END IF
    END IF

    CALL COMM.Talk(msgFrom$, to$, msg$)
    SELECT CASE COMM.Result
        CASE -1     ! 発言なし。
            PRINT "問題なし。"
        CASE 0
            PRINT "発言失敗。"
        CASE 1
            PRINT "発言成功。"
            IF myDL <> 0 THEN
                board(myPy, myPx) = myColor      ! 石を置く。
                ! 相手石を裏返す。
                CALL RevDirLength(board, myPy, myPx, myColor, myDL)
            END IF
        CASE 2     ! 発言の前にメッセージが到着していた。
            PRINT "メッセージ受信。From:" & msgFrom$ & " " & msg$
            IF to$ <> "" THEN
                PRINT "発言は失敗。"
            END IF

            LET mPos = MsgToPos(msg$)    ! 位置指定文字を数値に変換する。
            LET revPx = MOD(mPos,10)
            LET revPy = INT(mPos/10)
            IF IsLegalPlace(legalPlaces, revColor, revPy, revPx) <> 0 THEN
                ! 裏返せる相手石数 (= 自石数) を調査する。
                revDL = GetDirLength(board, revPy, revPx, revColor)
                board(revPy, revPx) = revColor   ! 相手石を置く。
                ! 自石を裏返す。
                CALL RevDirLength(board, revPy, revPx, revColor, revDL)
            END IF
    END SELECT
LOOP

LET counts = CountingStone(board)    ! 黒、白それぞれの数を数える。
black = MOD(counts,100)
white = INT(counts/100)
PRINT "black:" & STR$(black) & " / white:" & STR$(white)

END


MERGE "othellotool.BAS" MERGE "comm.BAS"
*4黒番は 1、白番は 2 を設定してください。
comm.BASexLock.BAS は black.BAS と同じ場所へ置いてください。

<テキスト23> ファイル名「white.BAS」(白番=後手)
<テキスト22> との違いは「自分の石の色*4」の 1 行だけなので、ファイルをコピーして編集すると簡単ですが、
コピー&ペーストをしたい人のために掲載します。(キーボードに慣れている人は手で打ち込んでください)

LET myColor = 2  ! 自分の石の色*4。(1:黒 / 2:白)
LET revColor = 1 + 1 - (myColor - 1) ! 石の反対色を求める。

! 自分と相手の識別名を用意する。
DIM commId$(2)
LET commId$(1) = "black"
LET commId$(2) = "white"

DECLARE EXTERNAL SUB COMM.Init
DECLARE EXTERNAL SUB COMM.Quiet
DECLARE EXTERNAL SUB COMM.Talk

CALL COMM.Init(commId$(myColor), "R:\")   ! 自分の識別名と伝言台を指定。
CALL COMM.Quiet

DECLARE EXTERNAL SUB SetBoard
DECLARE EXTERNAL SUB ListLegalPlaces
DECLARE EXTERNAL FUNCTION LengthLegalPlaces
DECLARE EXTERNAL SUB BoardDisplay
DECLARE EXTERNAL FUNCTION MsgToPos
DECLARE EXTERNAL FUNCTION IsLegalPlace
DECLARE EXTERNAL FUNCTION GetDirLength
DECLARE EXTERNAL SUB RevDirLength
DECLARE EXTERNAL FUNCTION CountingStone

! 盤面を用意する。
DIM board(8,8)
CALL SetBoard(board)

! 石を置ける場所のリスト。(y * 10 + MOD(x,10))
DIM legalPlaces(2, 8 * 8 - 4 + 1)  ! 1:黒番のリスト 2:白番のリスト。

DO
    ! 石を置ける場所を探す。
    CALL ListLegalPlaces(legalPlaces, 1, board) ! 黒番の分。
    CALL ListLegalPlaces(legalPlaces, 2, board) ! 白番の分。

    ! 石を置ける場所の数を求める。
    LET myAvailLength  = LengthLegalPlaces(legalPlaces, myColor)     ! 自分の場所数。
    LET revAvailLength = LengthLegalPlaces(legalPlaces, revColor)    ! 相手の場所数。

    ! 盤面を表示する。
    CALL BoardDisplay(board, legalPlaces, myColor)

    IF myAvailLength = 0 THEN
        IF revAvailLength = 0 THEN
            PRINT "双方共に石を置く場所がありません。終局します。"
            EXIT DO
        ELSE
            PRINT "石を置く場所がありません。パスしてください。"
        END IF
    END IF

    ! メッセージ送信の入力。
    ! 送りたいメッセージがなければ受信だけを行う。
    LET to$ = ""
    INPUT PROMPT "内容?" & "-> " & commId$(revColor) & " ": msg$
    IF msg$ = "." THEN
        PRINT "終了します。"
        EXIT DO
    END IF

    LET myDL = 0
    IF msg$ <> "" THEN
        LET mPos = MsgToPos(msg$)    ! 位置指定文字を数値に変換する。
        LET myPx = MOD(mPos,10)
        LET myPy = INT(mPos/10)
        IF myPx = 0 OR myPy = 0 THEN
            PRINT "置く石の位置は a1~h8 で指定してください。"
        ELSEIF IsLegalPlace(legalPlaces, myColor, myPy, myPx) = 0 THEN
            PRINT "石を置けません。"
        ELSE
            LET to$ = commId$(revColor)
            ! 裏返せる相手石数を調査する。
            LET myDL = GetDirLength(board, myPy, myPx, myColor)
        END IF
    END IF

    CALL COMM.Talk(msgFrom$, to$, msg$)
    SELECT CASE COMM.Result
        CASE -1     ! 発言なし。
            PRINT "問題なし。"
        CASE 0
            PRINT "発言失敗。"
        CASE 1
            PRINT "発言成功。"
            IF myDL <> 0 THEN
                board(myPy, myPx) = myColor      ! 石を置く。
                ! 相手石を裏返す。
                CALL RevDirLength(board, myPy, myPx, myColor, myDL)
            END IF
        CASE 2     ! 発言の前にメッセージが到着していた。
            PRINT "メッセージ受信。From:" & msgFrom$ & " " & msg$
            IF to$ <> "" THEN
                PRINT "発言は失敗。"
            END IF

            LET mPos = MsgToPos(msg$)    ! 位置指定文字を数値に変換する。
            LET revPx = MOD(mPos,10)
            LET revPy = INT(mPos/10)
            IF IsLegalPlace(legalPlaces, revColor, revPy, revPx) <> 0 THEN
                ! 裏返せる相手石数 (= 自石数) を調査する。
                revDL = GetDirLength(board, revPy, revPx, revColor)
                board(revPy, revPx) = revColor   ! 相手石を置く。
                ! 自石を裏返す。
                CALL RevDirLength(board, revPy, revPx, revColor, revDL)
            END IF
    END SELECT
LOOP

LET counts = CountingStone(board)    ! 黒、白それぞれの数を数える。
black = MOD(counts,100)
white = INT(counts/100)
PRINT "black:" & STR$(black) & " / white:" & STR$(white)

END


MERGE "othellotool.BAS" MERGE "comm.BAS"

<テキスト24> ファイル名「othellotool.BAS」
· ファイル保存時の文字コードを Shift-JIS にすること。
·「black.BAS」「white.BAS」と同じフォルダーに置くこと。
(%LOCALAPPDATA%\VirtualStore\Program Files (x86)\Decimal Basic\BASICw32\ の中でも OK)
! 盤面に初期の石を配置する。
EXTERNAL SUB SetBoard(board(,))
    DATA 0,0,0,0,0,0,0,0
    DATA 0,0,0,0,0,0,0,0
    DATA 0,0,0,0,0,0,0,0
    DATA 0,0,0,2,1,0,0,0
    DATA 0,0,0,1,2,0,0,0
    DATA 0,0,0,0,0,0,0,0
    DATA 0,0,0,0,0,0,0,0
    DATA 0,0,0,0,0,0,0,0
    MAT READ board
END SUB


! 位置指定文字を数値に変換する。例: "a1" -> 101 EXTERNAL FUNCTION MsgToPos(msg$) LET x = 0 LET y = 0 LET pos1$ = LCASE$(msg$(1:1)) LET pos2$ = LCASE$(msg$(2:2)) LET x = POS("abcdefgh", pos1$) LET y = POS("12345678", pos2$) LET MsgToPos = y * 10 + MOD(x, 10) END FUNCTION
! 指定位置から色の連続数を求める。 EXTERNAL FUNCTION GetSeries(board(,), y, x, dy, dx, c) LET h = UBOUND(board, 1) n = 0 DO IF NOT (1 <= y AND y <= h) THEN EXIT FOR LET w = UBOUND(board, 2) IF NOT (1 <= x AND x <= w) THEN EXIT FOR IF board(y, x) <> c THEN EXIT FOR LET n = n + 1 LET y = y + dy LET x = x + dx LOOP GetSeries = n END FUNCTION
! 石を置いたときに裏返せる全方向の相手石数を調査する。 EXTERNAL FUNCTION GetDirLength(board(,), y, x, c) LET h = UBOUND(board, 1) ! 盤面の高さを求める。 LET w = UBOUND(board, 2) ! 盤面の幅を求める。 LET revColor = 1 + 1 - (c - 1) ! 石の反対色を求める。 DIM dirLength(8) LET dirLength(1) = 0 ! 上 LET dirLength(2) = 0 ! 右上 LET dirLength(3) = 0 ! 右 LET dirLength(4) = 0 ! 右下 LET dirLength(5) = 0 ! 下 LET dirLength(6) = 0 ! 左下 LET dirLength(7) = 0 ! 左 LET dirLength(8) = 0 ! 左上 ! 上を調査する。 LET n = GetSeries(board, y - 1, x + 0, -1, 0, revColor) IF n > 0 AND y - n > 1 AND board(y - n - 1, x) = c THEN dirLength(1) = n ! 右上を調査する。 LET n = GetSeries(board, y - 1, x + 1, -1, +1, revColor) IF n > 0 AND y - n > 1 AND x + n < w AND board(y - n - 1, x + n + 1) = c THEN dirLength(2) = n ! 右を調査する。 LET n = GetSeries(board, y + 0, x + 1, 0, +1, revColor) IF n > 0 AND x + n < w AND board(y, x + n + 1) = c THEN dirLength(3) = n ! 右下を調査する。 LET n = GetSeries(board, y + 1, x + 1, +1, +1, revColor) IF n > 0 AND y + n < h AND x + n < w AND board(y + n + 1, x + n + 1) = c THEN dirLength(4) = n ! 下を調査する。 LET n = GetSeries(board, y + 1, x + 0, +1, 0, revColor) IF n > 0 AND y + n < h AND board(y + n + 1, x) = c THEN dirLength(5) = n ! 左下を調査する。 LET n = GetSeries(board, y + 1, x - 1, +1, -1, revColor) IF n > 0 AND y + n < h AND x - n > 1 AND board(y + n + 1, x - n - 1) = c THEN dirLength(6) = n ! 左を調査する。 LET n = GetSeries(board, y + 0, x - 1, 0, -1, revColor) IF n > 0 AND x - n > 1 AND board(y, x - n - 1) = c THEN dirLength(7) = n ! 左上を調査する。 LET n = GetSeries(board, y - 1, x - 1, -1, -1, revColor) IF n > 0 AND y - n > 1 AND x - n > 1 AND board(y - n - 1, x - n - 1) = c THEN dirLength(8) = n LET GetDirLength = & ! 関数から複数の値を返せないので返戻値に複数の値を入れる。 & dirLength(8) * 10^7 & &+ dirLength(7) * 10^6 & &+ dirLength(6) * 10^5 & &+ dirLength(5) * 10^4 & &+ dirLength(4) * 10^3 & &+ dirLength(3) * 10^2 & &+ dirLength(2) * 10^1 & &+ dirLength(1) * 10^0 END FUNCTION
! 相手石を裏返す。 EXTERNAL SUB RevDirLength(board(,), py, px, bw, dl) DIM dirLength(8) ! アーカイブされた裏返し可能状態を分解する。 ! 十進 BASIC は Perl と同様ランタイムコンパイル型のインタプリタなので、 ! 冗長な式が内部的に最適化されることを期待する。 LET dirLength(1) = MOD(INT(dl / 10^0), 10) LET dirLength(2) = MOD(INT(dl / 10^1), 10) LET dirLength(3) = MOD(INT(dl / 10^2), 10) LET dirLength(4) = MOD(INT(dl / 10^3), 10) LET dirLength(5) = MOD(INT(dl / 10^4), 10) LET dirLength(6) = MOD(INT(dl / 10^5), 10) LET dirLength(7) = MOD(INT(dl / 10^6), 10) LET dirLength(8) = MOD(INT(dl / 10^7), 10) ! 上下左右増分の符号 DATA -1, 0 ! 上側 DATA -1, +1 ! 右上側 DATA 0, +1 ! 右側 DATA +1, +1 ! 右下側 DATA +1, 0 ! 下側 DATA +1, -1 ! 左下側 DATA 0, -1 ! 左側 DATA -1, -1 ! 左上側 DIM deltaSign(8, 2) MAT READ deltaSign ! 相手石を裏返す。 FOR i = 1 TO 8 FOR j = 1 TO dirLength(i) LET board(py + j * deltaSign(i, 1), px + j * deltaSign(i, 2)) = bw NEXT j NEXT i END SUB
! 石を設置できる場所を調査する。 EXTERNAL SUB ListLegalPlaces(lp(,), bw, board(,)) LET pos = 1 FOR y = 1 TO UBOUND(board, 1) FOR x = 1 TO UBOUND(board, 2) IF board(y, x) = 0 AND GetDirLength(board, y, x, bw) <> 0 THEN LET lp(bw, pos) = y * 10 + MOD(x, 10) LET pos = pos + 1 END IF NEXT x NEXT y LET lp(bw, pos) = 0 END SUB
! 指定位置が設置可能リストに含まれるかどうかを返す。 ! 0: 含まれない。 ! 1~60: 含まれる。 EXTERNAL FUNCTION IsLegalPlace(lp(,), bw, y, x) IsLegalPlace = 0 LET v = y * 10 + MOD(x, 10) LET p = 1 DO LET n = lp(bw, p) IF n = 0 THEN EXIT DO IF n = v THEN LET IsLegalPlace = p EXIT DO END IF p = p + 1 LOOP END FUNCTION
! 盤面を表示する。 EXTERNAL SUB BoardDisplay(board(,), lp(,), v) mark$ = "□●○" PRINT " a b c d e f g h" ! 横のインデックスを表示する。 FOR y = 1 TO UBOUND(board, 1) PRINT y; ! 縦のインデックスを表示する。 FOR x = 1 TO UBOUND(board, 2) IF IsLegalPlace(lp, v, y, x) <> 0 THEN PRINT "・"; ! 石を置ける場所を示す。 ELSE LET n = board(y, x) + 1 PRINT mark$(n:n); ! 黒白の石を表示する。 END IF NEXT x PRINT NEXT y END SUB
! 黒、白それぞれの数を数える。 ! 10 進数 wwbb の形式で返すため黒は 99 以下である必要がある。(オセロは最大でも 64) EXTERNAL FUNCTION CountingStone(board(,)) LET nWhite = 0 LET nBlack = 0 FOR i = 1 TO UBOUND(board, 1) FOR j = 1 TO UBOUND(board, 2) c = board(i, j) IF c = 1 THEN nBlack = nBlack + 1 IF c = 2 THEN nWhite = nWhite + 1 NEXT j NEXT i CountingStone = nWhite * 100 + nBlack END FUNCTION
! 設置可能リストの長さを求める。 EXTERNAL FUNCTION LengthLegalPlaces(lp(,), bw) LET legalLen = 0 FOR i = 1 TO UBOUND(lp, 2) IF lp(bw, i) = 0 THEN EXIT FOR LET legalLen = legalLen + 1 NEXT i LET LengthLegalPlaces = legalLen END FUNCTION
2-2. オセロプログラムの実行
black.BAS, white.BAS の両方を起動してから対戦を実験してください。
(下記例では、読みやすくするために行間を広げています。実際は広がっていません)

プログラムで先手・後手の制限を設けていないので、どちらが先でも打ててしまいます。
また、相手の隙をついて連続で打つことも可能です。
動作確認のために、まずは基本ルールで対戦してください。

black(黒番=先手) を起動
内容?-> white
 
   a b c d e f g h
 1 □□□□□□□□
 2 □□□□□□□□
 3 □□□・□□□□
 4 □□・○●□□□
 5 □□□●○・□□
 6 □□□□・□□□
 7 □□□□□□□□
 8 □□□□□□□□

内容?-> white
d3
   a b c d e f g h
 1 □□□□□□□□
 2 □□□□□□□□
 3 □□□●□□□□
 4 □□□●●□□□
 5 □□□●○・□□
 6 □□□□・□□□
 7 □□□□□□□□
 8 □□□□□□□□



























内容?-> white
   a b c d e f g h
 1 □□□□□□□□
 2 □□□□□□□□
 3 □・○●□□□□
 4 □□・○●□□□
 5 □□□●○・□□
 6 □□□□・□□□
 7 □□□□□□□□
 8 □□□□□□□□

内容?-> white
c4
   a b c d e f g h
 1 □□□□□□□□
 2 □□・□□□□□
 3 □・○●□□□□
 4 □□●●●□□□
 5 □□□●○・□□
 6 □□□□・□□□
 7 □□□□□□□□
 8 □□□□□□□□


経過が長いので省略します


内容?-> white
   a b c d e f g h
 1 ●○○○○○○●
 2 ●●●●●●○●
 3 ●●●○●●○●
 4 ●●●●●●○●
 5 ●●●●●●○●
 6 ●●●●●●○●
 7 ●○○○○○○●
 8 ○●●●●○○●
双方共に石を置く場所がありません。終局します。
black:43 / white:21
   white(白番=後手) を起動
内容?-> black
 
   a b c d e f g h
 1 □□□□□□□□
 2 □□□□□□□□
 3 □□□□・□□□
 4 □□□○●・□□
 5 □□・●○□□□
 6 □□□・□□□□
 7 □□□□□□□□
 8 □□□□□□□□














内容?-> black
   a b c d e f g h
 1 □□□□□□□□
 2 □□□□□□□□
 3 □□・●・□□□
 4 □□□●●□□□
 5 □□・●○□□□
 6 □□□□□□□□
 7 □□□□□□□□
 8 □□□□□□□□

内容?-> black
c3
   a b c d e f g h
 1 □□□□□□□□
 2 □□□・□□□□
 3 □□〇●・□□□
 4 □□□〇●・□□
 5 □□・●○□□□
 6 □□□・□□□□
 7 □□□□□□□□
 8 □□□□□□□□




























経過が長いので省略します


内容?-> black
g1
   a b c d e f g h
 1 ●○○○○○○●
 2 ●●●●●●○●
 3 ●●●○●●○●
 4 ●●●●●●○●
 5 ●●●●●●○●
 6 ●●●●●●○●
 7 ●○○○○○○●
 8 ○●●●●○○●
双方共に石を置く場所がありません。終局します。
black:43 / white:21
2-3. オセロプログラムの説明
2-3-1. プログラムの構成
プログラムは個別部 (主プログラム) と共通部 (主プログラム以外) の 2 種類に分かれます。
プログラムの構成図 [1]
主プログラムblack.BASwhite.BAS
othellotool.BAS
便利プロシージャ群

|
SetBoard, BoardDisplay, MsgToPos,
GetDirLength, RevDirLength, CountingStone,
(GetSeries),
ListLegalPlaces, LengthLegalPlaces, IsLegalPlace

|
comm.BAS抽象化ブロックInit, Quiet, Talk
実務ブロックSendMessage, RecvMessage, WipeMessage ↔ メッセージ伝言台
ライブラリexLock_lock, exLock_unlock ↔ 排他制御
プロシージャ」とは、いくつかの命令・処理をひとつにまとめたもので、副プログラムと外部関数の両方を指します。
個別部
個別部は black.BAS と white.BAS で、プレイヤーごとに存在します。
人間に近い部分で利便性を図るプログラムを記述しています。

相手の着手 (石を打つこと) を受け取り、自分の着手を相手に伝える部分です。
比較的自由度の高いプログラムになります。

ここでは、black.BAS と white.BAS の違いがほとんどありませんが、
同じプログラムにする必要はありません。
会話のルールさえ守っていれば、どのようにでも記述できます。
自分の着手をマウスで指定できるようにしたり、音を出す改造をしても良いかもしれません。
共通部
共通部は othellotool.BAS と comm.BAS, exLock.BAS です。
comm.BAS, exLock.BAS は前項と同じなのでそちらを参照してください。

othellotool.BAS はオセロにあると便利な副プログラムと外部関数を集めて記述しています。
将棋の達人の様に棋譜を頭の中だけで動かせる人であれば、無くても構わないプログラムです。
comm.BAS や exLock.BAS とは全く関係せず、個別部の利便性を図る目的のためだけに存在しています。
2-3-2. プログラムの内容
このプログラム群は、トップダウンボトムアップを同時にスパイラルしながらも疎結合の割合を高くし、最終的にウォーターフォールにするという、割と難度の高い手法で組んでいます。日本人が最も不得意とする手法です。

肝となるのはパイププログラミング*5という手法です。この手法を骨子にして組むと、見通しの良いプログラムを記述できます。
これを実現するために、今回は全体を通して疑似リスト構造*6を取り入れています。
クラスの概念があれば独自構造を隠蔽化して実装することもできますが、十進 BASIC はそれがないため、プログラムと読解力⁉を工夫する必要があります。
*5UNIX のパイプをプログラムに応用したような手法です。30 年近く前に流行りかけたものの、理解できなかったのか重要視されなかったのか、業界に行き渡ることはありませんでした。いまでは Google に聞いても知らないようです。
その代わりフレームワーク等の、技術者のレベルが低くてもそれなりの保守性を確保できる環境が流行っています。
*6本来の「リスト構造」は「連結リスト」といってポインタで連結した要素で構成されますが、ここではただの 1 次元配列で強引に構成しています。

本稿の「個別部」は、共通部を適宜呼んでいる構造になっていることに加え、コメントに記述してあるので、説明の必要はないと思います。
なので「共通部」の othellotool.BAS について説明します。
(comm.BAS と exLock.BAS については前稿を参照してください)

すべて個別部から呼ばれます。
項番プロシージャ名説明
1SetBoard個別プログラムでは board(8,8) という配列変数を使って盤面の状況を管理します。
当プロシージャは、その盤面 borard(8,8) に初期状態として未着手(0) / 黒石(1) / 白石(2) の状態を作成します。
2BoardDisplay配列変数 board(8,8) がそのままでは読みにくいので記号で表示します。
また、石を置ける場所 (着手可能位置) も記号で表示します。
石を置ける場所は、第 2 引数の lp(黒or白, ..) という配列変数で受け付けます。
lp() は 2 次元配列をそのまま渡していますが、本来は 1 次元配列を渡すべき*7です。
*7十進 BASIC は、2 次元配列の一部を 1 次元配列として取り出すことができないので、このように渡しています。
3MsgToPos位置指定文字列 (a1 等) を横軸・縦軸の数値に変換します。
横軸は a~h または A~H、縦軸は 1~8 の文字である必要があります。

N = Y × 10 + X として値を返すので、呼び元で N を分解する必要があります。
規定外の文字列が渡された場合は 0 を返します。
4GetDirLength指定された横・縦を中心にして周囲 8 方向に相手の石がいくつ連続しているかを調査します。
★の右側に○が 3 個連続している例。
★○○○□
連続した相手の石のさらにひとつ向こうに自分の石があればその相手石の数を採用します。
相手の石が連続していても、その先に自分の石が存在しなければ、連続数を 0 にします。
例:
★の右側に○が 3 個連続しているが、その先に●がないので★に●は置けない。
★○○○□
右の連続数 → 0 とする。

例:
★の右側に○が 3 個連続しており、その先に●があるので★に●を置ける。
★○○○●
右の連続数 → 3 とする。
8 方向の値を返したいけれど十進 BASIC の関数が返せる値はひとつなので、
1 方向を 1 桁に割り当て、時計回りに最大 8 桁の数値に集約して値を返します。
例:  
 0  1  0 
30
042
  ⇒   1 0 0 2 4 0 3 0   集約  


03042001 (10 進数)
連続する石は 1 方向につきせいぜい 8 まで (ホントは 6) なのでこの方法が可能となっています。
(もしも 2 桁必要なら 102 ごとに集約すれば良い)

この説明では数字の並びが逆順になっていますが、コンピュータの数学ではこれが自然な形です。
X0·N0 + X1·N1 + ·· + Xn·Nn   ⇒    Z - 1 Xn·Nn
Σ
n = 0
N=底、Z=桁数

7 進数や 8 進数で計算しても以下のように良いことがあまりないため 10 進数で計算しています。
  • デバッグしにくくなる。
  • 扱う桁数が減らない。
5RevDirLength上記 GetDirLength で作成された値を各桁ごとに 8 方向分取り出し、その個数分を指定の石に変更します。
例:   03042001 (10 進数)   分解  


1 0 0 2 4 0 3 0   ⇒  
 0  1  0 
30
042
6CountingStone盤面の 2 次元配列 board(8,8) を参照し、黒石(1), 白石(2) それぞれの個数を求めます。
返す値は 白数 × 100 + 黒数 としているため、呼び元で返戻値を黒数と白数に分解する必要があります。

黒数、白数ともに最大でも 64 となり、10 進数 2 桁に収まるので 2 桁ずつをひとつの数値に集約しています。
7GetSeries単一方向に並ぶ同じ石の数を数えます。
GetDirLength の中から呼び出されます。主プログラムから呼び出されることはありません。
(必要なら主プログラムから呼び出しても構わないが、本プログラムでは必要がない)
8ListLegalPlaces黒または白の着手可能位置を配列 lp(黒or白, ..) に列挙します。

1 次元配列を個別に取り出すことができないのは*7で述べたとおりですが、それに加えて十進 BASIC ではリスト構造を表す型がないため、横軸・縦軸という 2 つの数値をひとつの数値にまとめ、最後の数値として 0 を入れる*8ことにより、疑似的に構造体のリストを作成しています。
(*8この時の 0 を終端子や終端記号と呼び、可変要素の最後を表します)

横軸と縦軸の数値はそれぞれ、せいぜい 8 までなので N = Y × 10 + X とすればひとつの数値に格納することができます。
「OPTION BASE 1*9」を変更していないので添え字が Y > 0, X > 0 となり、N に 0 が現れないことを利用しています。
    N1        N2     ··     Nn    0 (終端子)不要データ··不要データ
「OPTION BASE 0」とするなら、負数 (例: -1) 等を終端子にする必要があります。
(*9添え字は OPTION BASE 1 が標準。省略すると 1 のままになる)

この 1 次元配列 lp(黒or白, ..) は、以下 2 つの目的に使用しています。
  • 着手可能位置の表示
  • パスの要否の判断 (→ 終局の判断)
9LengthLegalPlaces1 次元配列を着手可能位置リストと見立て、長さを求めます。
2 次元配列から 1 次元配列を丸ごと取り出せない*7ため、引数に lp(黒or白, ..) という 2 次元配列を受け取っていますが、実際は 1 次元配列に対する処理にしています。

処理内容は単純で、先頭から 0 (終端子) が現れるまで数値の個数を数えます。
このリストには黒または白の石を置ける箇所がすべて格納されることを見込んでいるので、長さが 0 なら石を置くことができないことを表します。
10IsLegalPlace指定の横位置・縦位置が着手可能位置リストに含まれる場所を求めます。
着手の有効 / 無効を判別するために使用されます。

意味付けのために「場所を求める」という役割の関数にしています。


3. さらに対戦を実験

3-1. 他の実装と対戦する。
会話のルールさえ守られていれば、十進 BASIC ではない他のプログラムと対戦することができます。
こちらを参照して、異なる言語で作られた同じプログラムとの対戦を実験してください。
3-2. ネットワーク越しに対戦する。
プログラム exLock.BAS は、排他制御にディレクトリを使用しているため
SMB (Windows で構成するファイル共有) や、NFS (Linux のファイル共有) でも動作できます。

他の PC やファイルサーバ (NAS とも呼ばれます) に共有フォルダーを設定し、
そこを伝言台にすることにより、自分以外の PC との対戦を実験してください。
(共有フォルダーの設定は、PC の持ち主や LAN の管理者に相談してください)

伝言台にする PC やファイルサーバで必ずしもユーザプログラムが動作する必要はありません。
この章は、ここで終了です。