トリックテイキングをプログラミングで一人回しする【後編】




 この記事は、ボードゲーム・パズルプログラミング Advent Calendar 2022の23日目の記事です。

 【前編】の続きです。未読の方はそちらを先にご覧ください。
 今回は、前回作ったトリックテイキングの汎用コードを使って、実際に遊べるようにガワを整えていきます。

1.汎用PR層の実装
2.ホイスト
3.【自作】ジェネラス・スプリット
おわりに

※12/30追記:複数ディールのプレイに対応するため、「baseapp.py」を更新しました。ただし、ホイストのサンプルでは点数の累積を実装していません。ご了承ください。


1.汎用PR層の実装

 前回の汎用APを、TkInterで動かすためのガワを作ります。パッケージ構成は以下のとおりです。
modules
 ├ cardgame
 ├ playingcards
 └ tkgameview
 ロジックはcardgameとplayingcardsに入れており、TkInterはtkgameviewに入れます。
 ソースは2本です。全量を貼ります。長いですがご容赦ください。

 1本目はtkinter.Canvasを継承したゲーム用のcanvasです。細かい説明はソース内コメントを参照ください。
 プレイヤー名など最低限は表示します。手札はなんとなくアニメーションします。
 カード画像は /playingcards/gif/ に、たとえばハートのAなら「h01.gif」~「h13.gif」のように格納していますが、適当に書き換えてください。最初の定数 CARD_DIR_URL、CARD_EXT と、ファイル名を生成する getCardImageURL() を修正すればカスタマイズできます。

【canvas.py】
# coding: UTF-8
import tkinter as tk
import os, time
from PIL import Image, ImageTk

class GameCanvas(tk.Canvas):
    u'''TkInterのキャンバスを自作ゲーム用にカスタマイズしたクラス。トランプデッキに汎用対応。'''
    CARD_DIR_URL = os.path.dirname(__file__) + '/../playingcards/gif/'
    CARD_BACK = 'Z01'
    CARD_EXT = '.gif'
    CARD_WIDTH = 100
    CARD_HEIGHT = 150
    CARD_VISIBLE_AREA = 0.35
    BASE_FONT = ('Arial', 12)
    HAND_BASELINE_Y = 300
    
    def __init__(self, master=None, width=600, height=400, msg_x=0, msg_y=0):
        u'''コンストラクタ。キャンバスの縦横を設定し、自分でも値を保持しておく。'''
        super().__init__(master=master, width=width, height=height)
        self.width, self.height, self.msg_x, self.msg_y = width, height, msg_x, msg_y
        # カード画像を処理するためのリスト
        self.handImages = []
        self.playedCardImages = []
    
    def setBackground(self, bgcolor='green'):
        u'''キャンバス全体用の背景を作成する。'''
        self.delete('background')
        self.create_rectangle(0, 0, self.width, self.height, width=0, fill=bgcolor, tags='background')
        self.tag_lower('background')
    
    def showMsg(self, message, msg_x=None, msg_y=None, tags='message'):
        u'''メッセージ表示の汎用処理。
        座標を指定しない場合、オブジェクト作成時のデフォルト座標を使う。
        タグを指定しない場合、デフォルトのメッセージタグを使う。'''
        self.delete('message')
        if msg_x is None: msg_x = self.msg_x
        if msg_y is None: msg_y = self.msg_y
        self.create_text(msg_x, msg_y, text=message, tags=tags, fill='white', justify=tk.CENTER, font=self.BASE_FONT)
    
    def showPlayerNames(self, names, infos=[], coords=None):
        u'''プレイヤー名表示の汎用処理。
        座標を指定しない場合、内部設定のデフォルト座標を使う。'''
        self.delete('names')
        if coords is None:
            coords = [(190, 280), (30, 180), (260, 20), (570, 180)]
        for coord, name, info in zip(coords, names, infos):
            self.create_text(coord[0], coord[1], text=name + info, fill='white', tags='names', font=self.BASE_FONT)
    
    def waitClick(self, method):
        u'''画面にクリックエリアを用意する。クリックされたら引数に設定したイベントを呼ぶ。'''
        self.clickarea = self.create_rectangle(0, 0, self.width, self.height, width=0, fill='', tags='clickarea')
        self.tag_raise('clickarea')
        # クリックされると呼ばれるメソッド。クリックエリアを解除してから本処理に移る。
        def handler(event, self=self):
            self.delete('clickarea')
            method()
        self.tag_bind('clickarea', '', handler)
    
    def showDeck(self, deck, coord, trump=None, deckTop=None):
        u'''山札表示の汎用処理。切札表示、デッキトップ表示を切り替えられる。'''
        x, y = coord
        # 見出し
        self.create_text(x+50, y-25, text='DECK', fill='white', justify=tk.LEFT, tag='deckLabel')
        # 山札の大きさを設定
        self.deckImages = []
        self.delete('deckImage')
        deckLength = len(deck)
        # 切札があれば、表示して山札の大きさを-1する
        if trump is not None:
            self.delete('trump')
            im = Image.open(self.getCardImageURL(trump, isFront=True)).rotate(90, expand=True)
            self.trump = ImageTk.PhotoImage(im)
            self.create_image(x-70, y+15, anchor='nw', image=self.trump, tag='trump')
            deckLength -= 1
        # 山札を表示
        for i in range(deckLength):
            if (i + 1) == deckLength and deckTop is not None:
                self.deckImages.append(self.getCardImage(deckTop))
            else:
                self.deckImages.append(self.getCardImage(None, isFront=False))
            self.create_image(x + int(i/3), y - int(i/3), anchor='nw', image=self.deckImages[-1], tag='deckImage')
    
    def clearDeckImages(self):
        u'''山札画像をクリアする。'''
        self.delete('trump')
        self.delete('deckImage')
        self.delete('deckLabel')
        del self.deckImages[:]
        
    def unbindDeckTag(self):
        u'''デッキのタグをイベントからアンバインドする。'''
        self.tag_unbind('deckImage', '')
        
    def bindDeckTag(self, called):
        u'''デッキのタグをイベントにバインドする。
        引数calledはデッキをシングルクリックした時に呼び出すメソッド。'''
        def handler(event, self=self):
            called()
        self.tag_bind('deckImage', '', handler)
    
    def showHands(self, allHands, p):
        u'''プレイヤーの手札画像を手前に表示する。このメソッドは、実際のプレイに準じた表示を行う。
        引数allHandsは全プレイヤーの手札リスト、pはプレイヤーのインデクス。
        各要素に対応するトランプイメージを取得する。'''
        self.clearHandImages()
        hands = allHands[p]
        for i, hand in enumerate(hands):
            self.handImages.append(self.getCardImage(hand))
            x, y = self.cardPos(hands, i, self.CARD_VISIBLE_AREA)
            self.create_image(x, y, anchor='nw', image=self.handImages[i], tag=('hand' + str(i)))
        
    def cardPos(self, hand, idx, percent=None, baseline=None):
        u'''カードの手札位置を計算してタプル(x, y)を返す。
        percentはカードの左端からの表示面積割合。未指定の場合、定数CARD_VISIBLE_AREAが反映される。'''
        percent = self.CARD_VISIBLE_AREA if percent is None else percent
        card_x = self.CARD_WIDTH    # カード幅
        center_x = int((self.width - card_x * (1-percent)) / 2)   # センターライン(カードの重なり部分を加味)
        x = (center_x - len(hand) * int(card_x / (2 / percent)) + idx * int(card_x * percent))
        y = self.HAND_BASELINE_Y if baseline is None else baseline
        return (x, y)
    
    def clearHandImages(self):
        u'''手札画像をクリアする。'''
        for i, _ in enumerate(self.handImages):
            self.delete('hand' + str(i))
        del self.handImages[:]
        
    def unbindHandTags(self):
        u'''手札画像のタグをイベントからアンバインドする。'''
        for i, _ in enumerate(self.handImages):
            self.tag_unbind('hand' + str(i), '')
        
    def bindHandTags(self, called):
        u'''手札画像のタグをイベントにバインドする。
        引数calledは手札をダブルクリックした時に呼び出すメソッド。必ずインデクス引数iのみを取るように実装する。'''
        if len(self.handImages) < 1:
            return
        self.hDoubleClick = [0] * len(self.handImages)
        for i in range(len(self.handImages)):
            def handler(event, self=self, i=i):
                called(i)
            self.hDoubleClick[i] = self.tag_bind('hand' + str(i), '', handler)
        
    def getCardImage(self, card, isFront=True):
        u'''トランプのカード画像をtk.PhotoImageで取得する。isFrontがFalseの場合、cardが何であってもデフォルトの裏面を返す。'''
        return tk.PhotoImage(file=self.getCardImageURL(card, isFront))
    
    def getCardImageURL(self, card, isFront):
        u'''トランプのカード画像URLを取得する。'''
        if not isFront:
            return (self.CARD_DIR_URL + self.CARD_BACK + self.CARD_EXT)
        suit = card.getSuit().name[0:1].lower()
        rank = '{0:02d}'.format(card.getRank().value).replace('14', '01')
        return (self.CARD_DIR_URL + suit + rank + self.CARD_EXT)
    
    def deletePlayedCardImages(self):
        u'''プレイしたカード画像のリスト内容を削除する。'''
        del self.playedCardImages[:]
    
    def moveCard(self, player, coord_src, coord_dst, card, hand=-1, isFront=True):
        u'''開始座標から終了座標に向けて、カード画像をアニメーションする。
        playerはプレイヤー番号の整数。coord_src, coord_dstは開始・終了座標をタプルで指定する。
        cardは表示させるカードクラスの現物を指定する。ここから対応する画像を関数内で取得する。
        自分の手札である場合は、handで手札番号を渡すと、手札からそのカードを消す。不要の場合は無指定。
        isFrontがFalseの場合、cardが何であってもデフォルトの裏面を動かす。'''
        cardImage = self.getCardImage(card, isFront)
        if hand >= 0:
            self.delete('hand' + str(hand))
        # 引数座標のタプルを展開し、現在座標をセット
        src_x, src_y = coord_src
        dest_x, dest_y = coord_dst
        x, y = src_x, src_y
        # 増減をとる
        diff_x = dest_x - src_x
        diff_y = dest_y - src_y
        sign_x = ((diff_x > 0) - (diff_x < 0))
        sign_y = ((diff_y > 0) - (diff_y < 0))
        offset_x = sign_x * abs(diff_x / 10)
        offset_y = sign_y * abs(diff_y / 10)
        # 動かす
        while (x != dest_x or y != dest_y):
            if x != dest_x:
                x = x + offset_x
                if (sign_x > 0 and x > dest_x) or (sign_x < 0 and x < dest_x):
                    x = dest_x
            if y != dest_y:
                y = y + offset_y
                if (sign_y > 0 and y > dest_y) or (sign_y < 0 and y < dest_y):
                    y = dest_y
            self.delete('playcard' + str(player))
            self.playedCardImages.append(cardImage)
            self.create_image(x, y, anchor='nw', image=self.playedCardImages[-1], tag='playcard' + str(player))
            self.update()
            time.sleep(0.01)

 2本目はtkinter.Frameを継承したゲーム用のframeです。これがメインクラスになります。カードをダブルクリックすると遊べるので、よかったら試してください。
 なんか4人戦固定になってますね、すみません……。

【baseapp.py】
# coding: UTF-8

import tkinter as tk
from tkgameview.gamecanvas import GameCanvas as Canvas
from cardgame import inData
from cardgame.ttgame import TTGame as Game
from cardgame.playertype import PlayerType
from cardgame.eventtype import EventType as ev

class BaseApp(tk.Frame):
    u'''カードゲームのTkInter表示用ベースクラス。共通の処理をここに書く。単独でも実行可能。'''
    
    PLAYER = 0
    PLAYER_NUM = 4
    WINDOW_WIDTH = 600
    WINDOW_HEIGHT = 400
    MSG_X = 250
    MSG_Y = 130
    
    def __init__(self, master=None, title='', gameclass=Game, canvas=Canvas):
        u'''コンストラクタ。親フレームを作成し、描画とデータの初期設定を行う。'''
        # ガワ
        self.master = master
        super().__init__(self.master)
        self.pack()
        self.master.title(title)
        self.placeButtons()
        self.placeCanvas(canvas)
        # ゲームの初期設定
        self.gameclass = gameclass
        self.makeData()
        self.createTable()
    
    def placeButtons(self):
        u'''ボタンの初期配置を行う。'''
        self.replayButton = tk.Button(self, text=u'次のディール', fg='red', command=self.replay, state=tk.DISABLED)
        self.replayButton.grid(row=1, column=0, padx=10, sticky=tk.E)
        self.resetButton = tk.Button(self, text=u'最初から', fg='red', command=self.createTable)
        self.resetButton.grid(row=1, column=1, padx=10)
        self.quit = tk.Button(self, text=u' 終了 ', fg='red', command=self.master.quit)
        self.quit.grid(row=1, column=1, padx=10, sticky=tk.W)
    
    def replay(self, *event):
        u'''「次のディール」ボタン押下時の処理。得点情報を消さずに次のディールに続ける。'''
        self.event = self.game.start()
        self.inData['choice'] = None
        self.draw()
        self.replayButton.configure(state = tk.DISABLED)
        self.canvas.waitClick(self.play)
    
    def placeCanvas(self, canvas):
        u'''キャンバスの初期配置を行う。グリッド内のボタンの数分columnspanをとる。'''
        self.canvas = canvas(self, width=self.WINDOW_WIDTH, height=self.WINDOW_HEIGHT, msg_x=self.MSG_X, msg_y=self.MSG_Y)
        self.canvas.grid(row=0, column=0, columnspan=len([widget for widget in self.grid_slaves() if isinstance(widget, tk.Button)]))
    
    def makeData(self):
        u'''PR層側の初期入力を用意する。'''
        self.inData = inData.makeTestData(4)
        inData.setClear(True)
    
    def createTable(self, *event):
        u'''AP側のゲーム卓オブジェクトを作成し、ディール開始処理を行い、クリック待ちを入れる。'''
        self.game = self.gameclass(self.inData)
        self.event = self.game.start()
        self.draw()
        self.canvas.waitClick(self.play)
        
    def draw(self, message=None):
        u'''キャンバスの初期描画処理。背景、プレイヤー名、手札、打ち出しを表示する。'''
        self.canvas.setBackground()
        self.canvas.deletePlayedCardImages()
        self.canvas.showPlayerNames(self.event['PLAYER_NAMES'], self.getInfoList())
        self.canvas.showHands(self.event['HANDS'], self.PLAYER)
        if message is None:
            players = self.event['PLAYER_NAMES']
            message = players[self.event['DEALER']] + ' deals.\n' + players[self.event['OPENING_LEAD']] + ' plays first.\nClick screen.'
        self.canvas.showMsg(message)
    
    def play(self, *event):
        u'''ゲームプレイのメインルーチン。入力値を渡して次のAP処理を呼び、結果のイベントに応じた処理を行う。
        イベントは辞書型で返る。'''
        while True:
            self.event = self.game.next(self.inData)
            self.inData['choice'] = None
            # トリック開始
            if self.event['EVENT_TYPE'] == ev.BEGIN_TRICK:
                self.beginTrick()
            # ユーザの手番開始
            elif self.event['EVENT_TYPE'] == ev.USER_TURN:
                self.userTurnBegins()
                break
            # ユーザの手番実行
            elif self.event['EVENT_TYPE'] == ev.USER_APPROVED:
                self.approveMyCard()
            # 他プレイヤーの手番結果
            elif self.event['EVENT_TYPE'] == ev.OPPONENT_TURN:
                self.compTurn()
            # トリック解決
            elif self.event['EVENT_TYPE'] == ev.RESOLVE_TRICK:
                self.resolveTrick()
                self.canvas.waitClick(self.play)
                self.inData['clear'] = True
                break
            # 結果表示
            elif self.event['EVENT_TYPE'] == ev.DEAL_RESULT:
                self.showDealResult()
                self.replayButton.configure(state = tk.NORMAL)
                break
        # ここでループエンド
        
    def beginTrick(self):
        u'''トリック開始時処理。場と手札をリセットし、プレイヤー名(トリック数)を更新する。'''
        self.canvas.deletePlayedCardImages()
        self.canvas.showHands(self.event['HANDS'], self.PLAYER)
        self.canvas.showPlayerNames(self.event['PLAYER_NAMES'], self.getInfoList())
    
    def getInfoList(self):
        u'''取ったトリック数を、表示用の文字列リストにして返す。'''
        winlist = []
        for win_count in self.event['WIN_COUNTS']:
            winlist.append('\n(' + str(win_count) + ')')
        return winlist
        
    def userTurnBegins(self):
        u'''ユーザ手番の開始処理。メッセージを表示し、カードのクリック待ちをかける。'''
        if not self.event['IS_PLAYABLE']:
            self.canvas.showMsg('You cannot play\nthat card.')
        else:
            self.canvas.showMsg('Your turn.\n(Double-click)')
        self.canvas.bindHandTags(self.selectMyCard)
        
    def selectMyCard(self, choice):
        u'''ユーザの手札選択時の処理。選んだ手札番号を保存し、カードのクリック待ちを外してAPを呼ぶ。'''
        self.inData['choice'] = choice
        self.canvas.unbindHandTags()
        self.canvas.showMsg('')
        self.play()
        
    def approveMyCard(self):
        u'''ユーザの手札プレイ承認時の処理。選んだカードを実際に画面プレイに反映させる。'''
        choice = self.event['MY_CHOICE']
        coord_src = self.canvas.cardPos(self.event['HANDS'][self.PLAYER], choice, 0.35)
        coord_dst = (220, 150)
        self.canvas.moveCard(0, coord_src, coord_dst, self.event['PLAYED_CARDS'][-1], choice)
        
    def compTurn(self):
        u'''CPUの手札プレイ処理。CPUは自動で選ぶので、即画面プレイに反映させる。'''
        p = self.event['TURN_PLAYER']
        coords = [[(self.WINDOW_WIDTH / 2, 300), (220, 150)], [(-100, 100), (60, 100)], [(300, -200), (300, 0)], [(700, 100), (440, 100)]]
        self.canvas.moveCard(p, coords[p][0], coords[p][1], self.event['PLAYED_CARDS'][-1])
        
    def resolveTrick(self):
        u'''トリック解決処理。勝者を表示する。'''
        self.canvas.showMsg(self.event['PLAYER_NAMES'][self.event['TRICK_WINNER']] + ' wins.')
        
    def showDealResult(self):
        u'''ディール結果の表示処理。各プレイヤーの点数を表示する。'''
        self.canvas.deletePlayedCardImages()
        names = self.event['PLAYER_NAMES']
        scores = self.event['SCORES']
        self.canvas.showPlayerNames(names, self.getInfoList())
        self.canvas.showMsg('\n'.join([name + ': ' + str(score) for name, score in zip(names, scores)]))

if __name__ == '__main__':
    root = tk.Tk()
    app = BaseApp(master=root, title='トリックテイキング')
    app.mainloop()


2.ホイスト

 上のBaseAppだけでも動くし、ゲームにはなります。4人の個人戦でデッキは52枚、マストフォロー切札なしで、沢山トリックを取ったほうが勝ち。まあ、つまんないですよね。はい。配り運しかなくてゲームになってません。なのでもう少しゲームっぽいものに変えます。
 サンプルとして、実装が簡単なホイストを作ります。ルールはこちらを参照ください。1ディールごとの得点方式は私も知らなかったのでコードでは実装していませんが、得点計算を足す分にはそれほど困らないと思います。
 フォルダは好きなところに置いてください。汎用パッケージのmodulesを参照できればどこでもかまいません。

【WhistGame.py】は、APのメインロジックである TTGame クラスを継承しています。ホイスト用のディール時処理 WhistProcDeal を読ませるための上書きです。
# coding: UTF-8
from WhistProcDeal import WhistProcDeal as ProcDeal
from cardgame.ttgame import TTGame

class WhistGame(TTGame):
    u'''ホイストのゲーム卓。TTGameクラスに必要な設定を上書きする。'''
    
    def defineProc(self):
        u'''プロシージャ定義の上書き。'''
        super().defineProc()
        self.procdic['deal'] = ProcDeal()

【WhistProcDeal.py】は、APのディール開始時処理 ProcDeal を継承し、上書きします。配ったラストカードを切札として追加しています。
# coding: UTF-8
import random
from cardgame.procdeal import ProcDeal
from cardgame.eventtype import EventType as ev

class WhistProcDeal(ProcDeal):
    u'''独自ディール処理の実装。'''
    def setEvent(self, table):
        u'''切札を決めて、event出力に追加する。'''
        super().setEvent(table)
        table.event['TRUMP'] = table.players[table.dealer].getHand(-1)
        table.rule.setTrump(table.event['TRUMP'])

【Whist.py】は、BaseAppを継承してメインでゲームを動かすためのクラスです。これを実行してアプリを起動します。
# coding: UTF-8

import tkinter as tk
from tkgameview.baseapp import BaseApp
from WhistGame import WhistGame as Game

class App(BaseApp):
    u'''TkInter表示クラス。'''

    def __init__(self, master):
        u'''コンストラクタ。ゲームを変えて、親と同じ処理を行う。'''
        super().__init__(master, title=u'ホイスト', gameclass=Game)
    
    def draw(self):
        u'''キャンバスの初期描画処理。切札を表示し、親と同じ処理を行う。'''
        self.canvas.delete('trump')
        self.canvas.showMsg(message='TRUMP:\n' + self.event['TRUMP'].string(), msg_x=self.MSG_X, msg_y=80, tags='trump')
        super().draw()
    
    def showDealResult(self):
        u'''ディール結果の表示処理。各チームのトリック数合算を表示する。'''
        self.canvas.showPlayerNames(self.event['PLAYER_NAMES'], self.getInfoList())
        self.canvas.showMsg(
            'You : ' + str(self.event['WIN_COUNTS'][0] + self.event['WIN_COUNTS'][2])
            + '\nOpp : ' + str(self.event['WIN_COUNTS'][1] + self.event['WIN_COUNTS'][3])
        )
        
# アプリ実行(お決まり)
root = tk.Tk()
app = App(master=root)
app.mainloop()

 ベースのゲームをホイストに寄せて作ったから当然なのですが、個々のゲームを実装する際に継承だけで足りるのでコードが短くて済みます。なので、私自身の目的はまあまあ達成しています。


3.【自作】ジェネラス・スプリット

 ついでに、拙作もひとつ載せておきます。『ジェネラス・スプリット(Generous Split)』というゲームです。
 個人戦で、1トリック獲得につき3点ですが、トリックを一番多く取ると0点です。0トリックの場合は0点の人のトリック数をそのまま点数としてもらえます。手札は12枚で、切札はデッキトップをめくって決めます。切札で勝つとそのトリックは場の中央にプールされ、次に切札以外で勝った人が全部引き取ります。

【GenerousSplitGame.py】はAPのメインクラスの継承です。
# coding: UTF-8
from cardgame.ttgame import TTGame

from cardgame.table import Table
from rule import Rule
from playingcards.deck import Deck
from GenerousSplitProcDeal import GenerousSplitProcDeal as deal
from GenerousSplitProcTrickResult import GenerousSplitProcTrickResult as trickresult
from GenerousSplitProcDealResult import GenerousSplitProcDealResult as dealresult

class GenerousSplitGame(TTGame):
    u'''ゲーム卓。TTGameクラスに必要な設定を上書きする。'''
    
    def __init__(self, inData):
        u'''コンストラクタ。ルールを変更する。'''
        self.table = Table(Rule(len(inData['player_names'])), Deck(), inData)
        self.defineProc()
    
    def defineProc(self):
        u'''プロシージャ定義の上書き。'''
        super().defineProc()
        self.procdic['deal'] = deal()
        self.procdic['trickresult'] = trickresult()
        self.procdic['dealresult'] = dealresult()

【GenerousSplitProcDeal.py】はAPのディール処理の継承です。配り方や切札決めがちょっとだけ必要です。
# coding: UTF-8
from cardgame.procdeal import ProcDeal
from cardgame.eventtype import EventType as ev
from playingcards.deck import Deck

class GenerousSplitProcDeal(ProcDeal):
    u'''独自ディール処理の実装。'''
    
    def do(self, table):
        u'''山札がなければ作り直し、3人戦の場合は山札から2~4を抜く。'''
        # 山札チェック
        if len(table.deck.deck) < 52:
            del table.deck.deck[:]
            table.deck.deck.extend(Deck())
        # 3人戦の処理
        if len(table.players) == 3:
            newDeck = []
            for card in table.deck.deck:
                if card.rank > 4:
                    newDeck.append(card)
            table.deck.deck = newDeck
        # 親クラスのディール処理
        super().do(table)
    
    def setEvent(self, table):
        u'''切札を決めて、event出力に追加する。'''
        super().setEvent(table)
        table.event['CENTER_TRICKS'] = 0
        table.event['TRUMP'] = table.deck.pick()
        table.rule.setTrump(table.event['TRUMP'])

【GenerousSplitProcDealResult.py】はAPのディール結果判定の継承です。得点計算を素直に書いています。
# coding: UTF-8
from cardgame.proc import Proc
from cardgame.eventtype import EventType as ev

class GenerousSplitProcDealResult(Proc):
    u'''ディール結果判定の実装。'''
    
    def do(self, table):
        u'''1トリック3点。最多トリックは0点。ミゼールは最多獲得者のトリック数を頭割り。'''
        maxTricks = max(table.event['WIN_COUNTS'])
        miseres = sum(p==0 for p in table.event['WIN_COUNTS'])
        thisDealScore = [0] * len(table.players)
        for i, tricks in enumerate(table.event['WIN_COUNTS']):
            if tricks == maxTricks:
                thisDealScore[i] = 0
            elif tricks == 0:
                thisDealScore[i] = int(maxTricks / miseres)
            else:
                thisDealScore[i] = tricks * 3
            table.scores[i] += thisDealScore[i]
        table.event['SCORES'] = thisDealScore
        table.event['EVENT_TYPE'] = ev.DEAL_RESULT

【GenerousSplitProcTrickResult.py】はAPのトリック結果判定の継承です。切札勝ちのとき処理が変わるのを実装しました。
# coding: UTF-8
from cardgame.proctrickresult import ProcTrickResult
from cardgame.eventtype import EventType as ev

class GenerousSplitProcTrickResult(ProcTrickResult):
    u'''トリック結果判定の実装。'''
    
    def do(self, table):
        u'''トリック結果の判定。切札勝ちとサイドスート勝ちで処理を分ける。'''
        # トリックの勝者を取得
        winningCardIndex = table.rule.whoWins(table.playedCards)
        table.turn = (winningCardIndex + table.turn) % len(table.players)
        # 切札勝ちであれば中央のトリック数を増やし、サイドスートであればそれをつける
        count = 0
        if table.event['TRUMP'].suit == table.playedCards[winningCardIndex].suit and len(table.players[0].hands) > 0:
            table.event['CENTER_TRICKS'] += 1
        else:
            count = table.event['CENTER_TRICKS'] + 1
            table.event['CENTER_TRICKS'] = 0
        table.event['PLAYED_CARDS'] = table.playedCards
        table.event['TRICK_WINNER'] = table.turn
        table.event['WIN_COUNTS'][table.turn] += count
        table.event['EVENT_TYPE'] = ev.RESOLVE_TRICK

【rule.py】はAPのルールの継承です。手札枚数が基本の配りきりと少し変わります。
# coding: UTF-8
from cardgame.ttrule import TTRule

class Rule(TTRule):
    u'''独自ルール定義。'''
    
    def __init__(self, num):
        u'''コンストラクタ。人数に応じて手札枚数を設定する。'''
        handsize = 12
        if num > 4:
            handsize = 10
        super().__init__(handsize)

【App.py】はゲームを起動するメインクラスです。得点や切札の表示を少し変えます。
# coding: UTF-8
import tkinter as tk
from tkgameview.baseapp import BaseApp
from GenerousSplitGame import GenerousSplitGame as Game

class App(BaseApp):
    u'''TkInter表示クラス。'''
    
    def __init__(self, master):
        u'''コンストラクタ。ゲームを変えて、親と同じ処理を行う。'''
        super().__init__(master, title=u'Generous Split', gameclass=Game)
    
    def draw(self):
        u'''キャンバスの初期描画処理。切札を表示し、親と同じ処理を行う。'''
        self.canvas.delete('trump')
        msg = 'TRUMP: ' + self.event['TRUMP'].string() + '\nCENTER: ' + str(self.event['CENTER_TRICKS'])
        self.canvas.showMsg(message=msg, msg_x=self.MSG_X, msg_y=80, tags='trump')
        super().draw()
    
    def beginTrick(self):
        u'''トリック開始処理。センター表示を書き換える。'''
        super().beginTrick()
        self.canvas.delete('trump')
        msg = (
            'TRUMP: ' + self.event['TRUMP'].string()
            + '\nCENTER: ' + str(self.event['CENTER_TRICKS']))
        self.canvas.showMsg(message=msg, msg_x=self.MSG_X, msg_y=80, tags='trump')
        
    def getInfoList(self):
        u'''現在の合計点と、このディールの取ったトリック数を、表示用の文字列リストにして返す。'''
        scorelist = []
        for score, win in zip(self.event['SCORES'], self.event['WIN_COUNTS']):
            scorelist.append('\n' + str(win) + ' (' + str(score) + ' pts)')
        return scorelist
        
    def showDealResult(self):
        u'''ディール結果の表示処理。切札表示を消して、各プレイヤーの点数を表示する。'''
        self.canvas.delete('trump')
        super().showDealResult()
        
# アプリ実行(お決まり)
root = tk.Tk()
app = App(master=root)
app.mainloop()

 ビッドなしのゲームなので、ホイスト同様かなり簡単に作れました。これができたとき、汎用化しておいてよかったなと思いました。


おわりに

 人様にお見せするほどのコードではないのですが、せっかくの機会ですし私のツールを貼ってみました。そのうちgithubにでも入れておきます。
 ビッド処理を入れるとまた少し面倒なのですが、ビッドをするゲームは多いので、次にゲームを作るときそのロジックも追加しようと思います。でもビッドって最近のトリックテイキングでは好まれないんですよね。
 あと、ほかのトリックテイキングをkivyで動かすのを以前試していて、それも一応動くのですが、アプリ化するところまで作れてないので今回は見送りました。進展があれば公開します。

 ということで、良いクリスマスを!



<2022/12/23>


←No.46 トリックテイキングをプログラミングで一人回しする【前編】 No.48 コンテストの当落線上にひっかかるくらいのゲームの作り方→
コラム一覧へ トップページへ