Nocca NoccaをPythonで組む

Nocca NoccaをPythonで組む

お久しぶりです!

最近、家に帰ってもNetflixで韓ドラばっかり見ています。面白いですね、韓ドラ。

今回はボードゲーム「Nocca Nocca」をPythonで組むことを考えます。

Nocca Noccaとは?

「そもそも、Nocca Noccaとはなんぞや?」という人もいるともいます。

Nocca Noccaとはボードゲームの一種で、このようなものです(ノッカノッカのルール/インスト by ある|ボードゲーム情報より)

ゲームの進め方

  • ジャンケン等、お好きな方法で先手を決めます。
  • 交互に1コマづつ移動させていきます
    (将棋・チェスなどと同じように)。
  • コマは、前後左右斜めへ、1マス移動させることができます。
  • すべてのコマは同じ動きが出来ます。
  • 移動先のマス目に相手のコマ、もしくは自分のコマがあるとき、相手のコマ、もしくは自分のコマの上に自分のコマを置くことができます。これを「乗っかる」と呼びます。(もちろん乗らなくても構いません)
  • 乗っかられているコマを動かすことはできません。
  • 乗っかっているコマは、通常通り隣のマスへ移動することができます。
  • 乗っかっているコマに乗っかることができます。
  • コマの重なりは3段まで。4段目に乗っかる事はできません。

ゲームの目的・終了

  • 自分のコマのうちどれか1つが相手のゴールに侵入すれば勝利です。
  • ほとんど無いパターンですが、全てのコマに乗っかられてしまい、動かすコマがなければ、その時点で負けとなります。

ともあれ、これを実装してゆきましょう!

なぜこのゲームなのか

この前友人宅で飲み会を開いたときに、「ゲームした感じ面白そうだったから、組んでみるか」という軽いノリです。

実装

必要なもの

  • Python 3

今回はバニラ状態のPythonでも動きます。というか、そんなに高級なライブラリーを導入しなくても組めます。

このゲームの軽いルールを分かっていれば、所要時間としては1時間かかるかかからないかくらいです。実際私はルールが曖昧だったので、先ほどのリンクで調べながら書いていました笑

ボードの定義

まずはボードを定義してゆきます。

ボード自体は6行5列ですが、各セルには石を積めるということで、要素自体に配列を用意しないといけません。

「どうせスタックを使うわけだし、[None]*3の配列なんて用意する必要ないな」と思ったので、各要素には空の配列[]を入れています。

BLACK = 0
WHITE = 1


class NoccaNocca(object):

    def __init__(self):

        self.init_board()


    def init_board(self):

        self.board = [[[] for _ in range(5)] for _ in range(6)]
        for j in range(5):
            self.board[0][j].append(BLACK)
            self.board[5][j].append(WHITE)



def main():

    n = NoccaNocca()


if __name__ == '__main__':

    main()

ボードゲームをPythonで実装するときによくある問題として、Pythonは配列などをsharrow copyしかしないという問題です。なので[[[]*5]*6]とはせずに上のコードのように[[[] for _ in range(5)] for _ in range(6)]とする必要があります。

また、最初に石を配置しないといけないので、その部分をfor文で用意することとします。

さらに、先行を黒の石BLACKということにしました。別にどちらが先でも問題はないです。

ボードの中身を見やすくする

このままだとself.boardの中身が見やすくないので、print_board()として定義します。

class NoccaNocca(object):

###

    def print_board(self):

        print('{turn}\'s turn.'.format(
            turn={
                BLACK: 'BLACK',
                WHITE: 'WHITE'
            }[self.turn]
        ))
        print('i\j|', end='')
        print('   '.join(map(str, range(5))))
        print('---+', end='')
        print('-' * 5 * 4)

        for i in range(6):
            print('{i}  |'.format(
                i=i
            ), end='')
            print(' '.join(map(lambda j: ''.join(map(str, self.board[i][j])) + '_' * (3-len(self.board[i][j])), range(5))))

###

def main():

    n = NoccaNocca()
    n.print_board()

CUIで石が3段重なるように見せるは少々大変ですが、map()join()を駆使して少しでも見やすいようにします。

これを実行すると、このようになります。

BLACK's turn.
i\j|0   1   2   3   4
---+-------------------
0  |0__ 0__ 0__ 0__ 0__
1  |___ ___ ___ ___ ___
2  |___ ___ ___ ___ ___
3  |___ ___ ___ ___ ___
4  |___ ___ ___ ___ ___
5  |1__ 1__ 1__ 1__ 1__

それっぽく表示されました!

座標の入力

次に必要になるのが座標の入力部分です。

import itertools

###

class NoccaNocca(object):

    def __init__(self):

        self.init_board()
        self.is_continuing = True
        self.turn          = BLACK
        self.i             = None
        self.j             = None

        self.set_valid_ij_li() 

    ###

    def set_valid_ij_li(self):

        self.valid_ij_li = [(i, j) for i, j in itertools.product(range(6), range(5)) if len(self.board[i][j]) > 0 and self.board[i][j][-1] == self.turn]


    def select_ij(self, i: range(6), j: range(5)) -> bool:

        if (i, j) not in self.valid_ij_li:
            return False

        self.i = i
        self.j = j

        return True
    


def main():

    n = NoccaNocca()
    n.print_board()

    while n.is_continuing:
        while True:
            i, j = map(int, input('which stone?    [(i j)∈{status}]: '.format(
                status=', '.join(map(lambda ij: str(ij).replace(',', ' '), n.valid_ij_li))
            )).split(' '))
            if n.select_ij(i, j):
                break

set_valid_ij_li()で有効である座標指定をセットし、その中から選ばせようとしています。

適切な値となっていない場合は、Falseを返すようになっています。

指定した座標が有効である場合はclass変数に組み込みます(別にこうしなくても良いかもしれないが、わざわざ出力してまた入力して、ということはしたくないのでこうする)

方向の入力

次に座標を指定したら、方向に関する部分が必要になってきます。

Nocca Noccaでは端を除くに8方向に進めます。そのため端を除くように実装する必要があります。

また、Nocca Noccaでは石を3つまでしか積めないという制約があることから、指定した座標にある石の数が3となっていた場合は無効になるように設計します。

###
class NoccaNocca(object):

    def __init__(self):

        self.init_board()
        self.is_continuing = True
        self.turn          = BLACK
        self.i             = None
        self.j             = None
        self.direction     = None

        self.set_valid_ij_li() 


    ###

    def select_ij(self, i: range(6), j: range(5)) -> bool:

        if (i, j) not in self.valid_ij_li:
            return False

        self.i = i
        self.j = j

        self.set_valid_directions()

        return True
    

    def select_direction(self, direction: str) -> bool:

        if direction not in self.valid_directions:
            return False

        self.direction = direction

        return True


    def set_valid_directions(self):

        self.valid_directions = 'hjklyubn'
        if   self.i == 0 and self.turn == BLACK:
            self.valid_directions = self.valid_directions.replace('y', '').replace('k', '').replace('u', '')

        elif self.i == 5 and self.turn == WHITE:
            self.valid_directions = self.valid_directions.replace('b', '').replace('j', '').replace('n', '')

        if   self.j == 0:
            self.valid_directions = self.valid_directions.replace('y', '').replace('h', '').replace('b', '')

        elif self.j == 4:
            self.valid_directions = self.valid_directions.replace('u', '').replace('l', '').replace('n', '')

        for direction, (_i, _j) in {
            'y': (-1, -1), 'k': (-1,  0), 'u': (-1,  1),
            'h': ( 0, -1),                'l': ( 0,  1),
            'b': ( 1, -1), 'j': ( 1,  0), 'n': ( 1,  1)
        }.items():
            if direction in self.valid_directions and len(self.board[self.i+_i][self.j+_j]) == 3:
                self.valid_directions = self.valid_directions.replace(direction, '')



def main():

    n = NoccaNocca()

    while n.is_continuing:
        print('=' * 32)
        n.print_board()
        while True:
            i, j = map(int, input('which stone?    [(i j)∈{status}]: '.format(
                status=', '.join(map(lambda ij: str(ij).replace(',', ' '), n.valid_ij_li))
            )).split(' '))
            if n.select_ij(i, j):
                break

        while True:
            direction = input('which direction?[{status}]: '.format(
                status = n.valid_directions
            ))
            if n.select_direction(direction):
                break

replace()の部分がちょっと気持ち悪くて書き換えた方が良いのかもしれませんが(笑)、飛ばします。

方向はこのようになっています。

direction:
y   k   u
  \ | /  
h - * - l
  / | \  
b   j   n

また、select_ij()の中で有効な方向をセットするset_valid_directions()を実行するように書き換えました。

石の移動

最後に、動かしたい石と方向が決まったので、あとは石を動かすだけです。

###

class NoccaNocca(object):

    ###

    def move(self) -> str:

        _i, _j = {
            'y': (-1, -1), 'k': (-1,  0), 'u': (-1,  1),
            'h': ( 0, -1),                'l': ( 0,  1),
            'b': ( 1, -1), 'j': ( 1,  0), 'n': ( 1,  1)
        }[self.direction]

        i_ = self.i + _i
        j_ = self.j + _j

        if (self.turn == BLACK and i_ == 6) or (self.turn == WHITE and i_ == -1):
            self.is_continuing = False
            self.winner = self.turn

            return 'Won'

        self.board[i_][j_].append(self.board[self.i][self.j].pop())
        self.turn = BLACK if self.turn == WHITE else WHITE
        self.set_valid_ij_li() 

        if len(self.valid_ij_li) == 0:
            self.is_continuing = False
            self.winner = BLACK if self.turn == WHITE else WHITE

        return 'Moved'


    ###


def main():

    n = NoccaNocca()
    while n.is_continuing:
        print('=' * 32)
        n.print_board()
        while True:
            i, j = map(int, input('which stone?    [(i j)∈{status}]: '.format(
                status=', '.join(map(lambda ij: str(ij).replace(',', ' '), n.valid_ij_li))
            )).split(' '))
            if n.select_ij(i, j):
                break

        while True:
            direction = input('which direction?[{status}]: '.format(
                status = n.valid_directions
            ))
            if n.select_direction(direction):
                break

        n.move()

    print('=' * 32)
    print('{winner} won!'.format(
        winner={
            BLACK: 'BLACK',
            WHITE: 'WHITE'
        }[self.winner]
    ))

ここで大事なのが終了条件の判定です。

Nocca Noccaには「石を対岸に置く」か「すべての相手の石を動かせなくする」ことで勝利します。なので、それに見合うように実装します(最初、後ろの条件を無視して書いていた)

さいごに

いかがでしたでしょうか?

自分で組んでみた感じでは、Nocca Noccaを実装するのは、オセロほど大変ではありませんでした。

また他のボードゲームでプログラムしやすそうなのを見つけたら、組んでみます笑

プログラムの全体

プログラムの全体像はこのようになりました。

import itertools

BLACK  = 0
WHITE  = 1
HEIGHT = 6
WIDTH  = 5


class NoccaNocca(object):

    def __init__(self):

        self.init_board()
        self.is_continuing = True
        self.turn          = BLACK
        self.i             = None
        self.j             = None
        self.direction     = None

        self.set_valid_ij_li() 


    def init_board(self):

        self.board = [[[] for _ in range(WIDTH)] for _ in range(HEIGHT)]
        for j in range(WIDTH):
            self.board[0    ][j].append(BLACK)
            self.board[WIDTH][j].append(WHITE)


    def select_ij(self, i: range(HEIGHT), j: range(WIDTH)) -> bool:

        if (i, j) not in self.valid_ij_li:
            return False

        self.i = i
        self.j = j

        self.set_valid_directions()

        return True


    def select_direction(self, direction: str) -> bool:

        if direction not in self.valid_directions:
            return False

        self.direction = direction

        return True


    def move(self) -> str:

        _i, _j = {
            'y': (-1, -1), 'k': (-1,  0), 'u': (-1,  1),
            'h': ( 0, -1),                'l': ( 0,  1),
            'b': ( 1, -1), 'j': ( 1,  0), 'n': ( 1,  1)
        }[self.direction]

        i_ = self.i + _i
        j_ = self.j + _j

        if (self.turn == BLACK and i_ == HEIGHT) or (self.turn == WHITE and i_ == -1):
            self.is_continuing = False
            self.winner = self.turn

            return 'Won'

        self.board[i_][j_].append(self.board[self.i][self.j].pop())
        self.turn = BLACK if self.turn == WHITE else WHITE
        self.set_valid_ij_li() 

        if len(self.valid_ij_li) == 0:
            self.is_continuing = False
            self.winner = BLACK if self.turn == WHITE else WHITE

        return 'Moved'


    def set_valid_ij_li(self):

        self.valid_ij_li = [(i, j) for i, j in itertools.product(range(HEIGHT), range(WIDTH)) if len(self.board[i][j]) > 0 and self.board[i][j][-1] == self.turn]


    def set_valid_directions(self):

        self.valid_directions = 'hjklyubn'
        if   self.i == 0 and self.turn == BLACK:
            self.valid_directions = self.valid_directions.replace('y', '').replace('k', '').replace('u', '')

        elif self.i == HEIGHT - 1 and self.turn == WHITE:
            self.valid_directions = self.valid_directions.replace('b', '').replace('j', '').replace('n', '')

        if   self.j == 0:
            self.valid_directions = self.valid_directions.replace('y', '').replace('h', '').replace('b', '')

        elif self.j == WIDTH - 1:
            self.valid_directions = self.valid_directions.replace('u', '').replace('l', '').replace('n', '')

        for direction, (_i, _j) in {
            'y': (-1, -1), 'k': (-1,  0), 'u': (-1,  1),
            'h': ( 0, -1),                'l': ( 0,  1),
            'b': ( 1, -1), 'j': ( 1,  0), 'n': ( 1,  1)
        }.items():
            if direction in self.valid_directions and len(self.board[self.i+_i][self.j+_j]) == 3:
                self.valid_directions = self.valid_directions.replace(direction, '')


    def print_board(self):

        print(r'direction:')
        print(r'y   k   u')
        print(r'  \ | /  ')
        print(r'h - * - l')
        print(r'  / | \  ')
        print(r'b   j   n')
        print('-' * 32)
        print('{turn}\'s turn.'.format(
            turn={
                BLACK: 'BLACK',
                WHITE: 'WHITE'
            }[self.turn]
        ))

        print('i\j|', end='')
        print('   '.join(map(str, range(WIDTH))))
        print('---+', end='')
        print('-' * WIDTH * 4)

        for i in range(HEIGHT):
            print('{i}  |'.format(
                i=i
            ), end='')
            print(' '.join(map(lambda j: ''.join(map(str, self.board[i][j])) + '_' * (3-len(self.board[i][j])), range(WIDTH))))



def main():

    n = NoccaNocca()
    while n.is_continuing:
        print('=' * 32)
        n.print_board()
        while True:
            i, j = map(int, input('which stone?    [(i j)∈{status}]: '.format(
                status=', '.join(map(lambda ij: str(ij).replace(',', ' '), n.valid_ij_li))
            )).split(' '))
            if n.select_ij(i, j):
                break

        while True:
            direction = input('which direction?[{status}]: '.format(
                status = n.valid_directions
            ))
            if n.select_direction(direction):
                break

        n.move()

    print('=' * 32)
    print('{winner} won!'.format(
        winner={
            BLACK: 'BLACK',
            WHITE: 'WHITE'
        }[self.winner]
    ))


if __name__ == '__main__':

    main()