modules
├ cardgame
├ playingcards
└ tkgameview
ロジックはcardgameとplayingcardsに入れており、TkInterはtkgameviewに入れます。# 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)
# 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()
# 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()
# 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'])
# 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()
# 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()
# 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'])
# 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
# 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
# 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)
# 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()
←No.46 トリックテイキングをプログラミングで一人回しする【前編】 No.48 コンテストの当落線上にひっかかるくらいのゲームの作り方→
コラム一覧へ トップページへ