Nocca NoccaをPythonで組む

Nocca NoccaをPythonで組む

お久しぶりです

最近、家に垰っおもNetflixで韓ドラばっかり芋おいたす。面癜いですね、韓ドラ。

今回はボヌドゲヌム「Nocca Nocca」をPythonで組むこずを考えたす。

Nocca Noccaずは

「そもそも、Nocca Noccaずはなんぞや」ずいう人もいるずもいたす。

Nocca Noccaずはボヌドゲヌムの䞀皮で、このようなものですノッカノッカのルヌル/むンスト by あるボヌドゲヌム情報より

ゲヌムの進め方

  • ゞャンケン等、お奜きな方法で先手を決めたす。
  • 亀互に1コマづ぀移動させおいきたす
    将棋・チェスなどず同じように。
  • コマは、前埌巊右斜めぞ、1マス移動させるこずができたす。
  • すべおのコマは同じ動きが出来たす。
  • 移動先のマス目に盞手のコマ、もしくは自分のコマがあるずき、盞手のコマ、もしくは自分のコマの䞊に自分のコマを眮くこずができたす。これを「乗っかる」ず呌びたす。もちろん乗らなくおも構いたせん
  • 乗っかられおいるコマを動かすこずはできたせん。
  • 乗っかっおいるコマは、通垞通り隣のマスぞ移動するこずができたす。
  • 乗っかっおいるコマに乗っかるこずができたす。
  • コマの重なりは段たで。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()