《付録》exTetrimino3.py

# -*- coding: utf-8 -*-
#===============================================================================
#    Copyright (C) 2000-2008, 小泉ひよ子とタマゴ倶楽部
#
# Change History: Games
#    1988/05, Smalltalk
#    2004/09, Java
#    2005/02, C#
#    2005/03, Jython
# History: WPF examples
#    2008/01/25, IronPython 1.1.1 (download)
#    2008/08/22, IronPython 1.1.2 (download)
#    2008/03/16, ver.2.0, WPF
#    2008/00/00, ver.2.1, IronPython 1.1.2 
#===============================================================================
from _ant import *
from System.Windows import *
from System.Windows.Controls import *
from System.Windows.Input import *

from Tetrimino3 import *
## --------------------
class TestCase:
    types = "COpbYZLIJS"

    def __iter__(self):
        m = self.items()
        for e in self.types:
            yield e, m[e]

    def items(self):
        return dict((e, eval("Tetrimino%s(4, 4)"%e.upper()))
            for e in self.types)

## --------------------               
class ExWindow(Window):
    def __init__(self, Content=None, **args):
        self.InitializeComponent(Content)
        self.init()

    def InitializeComponent(self, Content):
        self.Content = LoadXaml(Content)

    def _Controls(self, target):
        controls = xaml_controls(self)
        for e in target:
            setattr(self, e, controls[e])

    def init(self):
        target = "tabControl", "button",
        self._Controls(target)

        self.items = {}
        for e, mino in TestCase():
            tray = Tray()
            mino.tray = tray

            self.items[e] = mino
            item = TabItem(Header=e)
            self.tabControl.Items.Add(item)            
            panel = Canvas()
            item.Content = panel
            self.addTray(panel, tray)
            self.addMino(panel, mino)
            
        self.tabControl.SelectionChanged += self.selectionChanged
        self.button.Click += self.click
        self.KeyDown += self.keyDown

    def addTray(self, panel, tray):
        for k, tile in tray.leftEdges.items():
            panel.Children.Add(tile.shape)
        for k, tile in tray.rightEdges.items():
            panel.Children.Add(tile.shape)
        for k, tile in tray.tiles.items():
            panel.Children.Add(tile.shape)

    ## --------------------               
    def addMino(self, panel, mino):
        for e in mino.shape:
            panel.Children.Add(e)

    def selectionChanged(self, sender, e):
        self.mino = self.items[sender.SelectedItem.Header]
        self.button.Content = self.state()

    def click(self, sender, e):
        self.mino.rotateClockwise()
        sender.Content = self.state()

    def keyDown(self, sender, e):
        KeyValues(e.Key).switch({
            Key.Right: self.mino.shiftRight,
            Key.Left : self.mino.shiftLeft,
            Key.Up   : self.mino.counterClockwise,
            Key.Down : self.mino.rotateClockwise ,
            })
        self.button.Content = self.state()
        
    def state(self):
        return "Type-%s: rotation = %d"%(
            self.mino.__class__.__name__[-1], self.mino.phase)

class KeyValues:
    def __init__(self, value):
        self.value = value

    def switch(self, cases):
        for key, f in cases.items():
            if key == self.value:
                f()

## --------------------               
if __name__ == "__main__":
    import sys
    xaml = sys.argv[1]
    win = ExWindow(
        Title=__file__,
        Width=234, Height=230,
        Content=xaml,
        )
    Application().Run(win)

## --------------------               

テトリミノの盤面を構成する

class Tray:
    def _tiles(self):
        ...
        s = {}
        for x, y in [(e*m1+2, 0) for e in range(5)]:
            for _ in range(3):
                s[x, y] = Tile(x, y); y += m2
        for x, y in [(e*m1+3, 2) for e in range(4)]:
            for _ in range(2):
                s[x, y] = Tile(x, y); y += m2
        return s

    def _edges(self, x):
        ...
        s = {}
        for x, y in [(x-1, e*m) for e in range(5)]:
            for _ in range(3):
                s[x, y] = Edge(x, y); x += 1
        return s

新たなテストケースの下地となる盤面(トレイ)を構成します。テトリミノが自由に移動できる境界内には Tile を敷き詰め、その境界外には Edge を敷き詰めます。

class Ostone(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.shape = self._shape()

    def _shape(self):
        M = 2
        Ox = HexStone._dx; W = HexStone._width
        Oy = HexStone._dy; H = HexStone._height
        X = Ox + W*self.x
        Y = Oy + H*self.y

        points = self.pointCollection([Point(X + dx*M, Y + dy*M)
            for dx, dy in HexStone(X, Y, False).vertices])
        return Polygon(
            HorizontalAlignment=HorizontalAlignment.Center,
            VerticalAlignment=VerticalAlignment.Center,
            Stroke=self._strokeColor,
            Points=points,
            )

Tile/Edge に共通する特性(座標や6角形の形状など)を規定します。

class Tile(Ostone):
    def __init__(self, x, y):
        self._strokeColor = Brushes.Gray
        super(self.__class__, self).__init__(x, y)

class Edge(Ostone):
    def __init__(self, x, y):
        self._strokeColor = Brushes.LightGray
        super(self.__class__, self).__init__(x, y)

Tile/Edge に固有の特性を規定します。このテストケースでは、輪郭の色の違いを設定するだけです。

テストケースを記述する(3)

Jython で作成した)既存のモジュール hexagon.py を再利用しながら、新たなモジュールの動作を検証するために、テストケースを作成します。

class ExWindow(Window):
    def init(self):
        target = "tabControl", "button",
        self._Controls(target)

        self.items = {}
        for e, mino in TestCase():
            tray = Tray()
            mino.tray = tray
            self.items[e] = mino
            item = TabItem(Header=e)
            self.tabControl.Items.Add(item)            
            panel = Canvas()
            item.Content = panel
            self.addTray(panel, tray)
            self.addMino(panel, mino)

        self.tabControl.SelectionChanged += self.selectionChanged
        self.button.Click += self.click
        self.KeyDown += self.keyDown

ジェネレーター TestCase によって、10種類のテトリミノが得られます。さらに、各テトリミノごとに Tray を用意します。

    def addTray(self, panel, tray):
        for k, tile in tray.leftEdges.items():
            panel.Children.Add(tile.shape)
        for k, tile in tray.rightEdges.items():
            panel.Children.Add(tile.shape)
        for k, tile in tray.tiles.items():
            panel.Children.Add(tile.shape)

Tray を構成する各要素(leftEdges/rightEdges/tiles)をパネルに追加します。

    def keyDown(self, sender, e):
        KeyValues(e.Key).switch({
            Key.Right: self.mino.shiftRight,
            Key.Left : self.mino.shiftLeft,
            Key.Up   : self.mino.counterClockwise,
            Key.Down : self.mino.rotateClockwise,
            })
        self.button.Content = self.state()

矢印キーを選択すると、テトリミノを回転(↓|↑)シフト(←|→)できます。テストケース2と違って、ここでは引数を必要としないメソッド群を呼び出します。

class KeyValues:
    def switch(self, cases):
        for key, f in cases.items():
            if key == self.value:
                f()

押されたキーに対応するアクションを起動します。テストケース2と違って、ここでは引数を必要としないメソッド f を呼び出します。

テトリミノの状態を更新する

Jython で作成した)既存のモジュール hexagon.py を再利用しながら、新たなモジュールの動作を検証するために、テストケースを作成します。

class Omino(object):
    def rotate(self, sign):
        self.phase = self._rotate1(1, sign)
        for e, n in zip(self.shape[self.offset:], self.mino1):
            e.Points = self.matrix1[self._rotate1(n)]
        self._update()      # here goes ... (^.^)

    def shift(self, n=0):
        self.x += n
        self.matrix1 = self._matrix(self.x, self.y, self._mat1)
        W = HexStone._width
        for s in self.shape:
            s.Points = self.pointCollection([Point(e.X + n*W, e.Y)
                for e in s.Points])
        self._update()      # here goes ... (^.^)

テトリミノの回転/シフトに伴って、その状態を更新 self._update() します。

    def _update(self):
        s = self.spots()
        if self.any([e in self.tray.leftEdges  for e in s]):
            self.shiftRight()   # left -> right
        if self.any([e in self.tray.rightEdges for e in s]):
            self.shiftLeft()    # right -> left

    def all(self, s):           # compensate for Python 2.5
        return False not in s
    
    def any(self, s):           # compensate for Python 2.5
        return True in s

テトリミノの回転/シフトに伴って、その状態を更新します。領域内に収まらないときには、テトリミノを(必要なだけ)左右にシフトします。ここで注意して欲しいのは、相互参照による再帰呼び出しになっていることです。さもないと、無限ループに陥りかねません。


《Note》組み込み関数 all/any:Python 2.5/IronPython 1.1.1 の互換性の問題から、組み込み関数 all/any に準拠したメソッドを用意しました。ただし、これらのメソッドの本体は、テストケースに特化したもので、組み込み関数 all/any との完全互換性には配慮していません。IronPython の次期リリースを見込んだ、暫定的な措置と考えてください。また「プロセス指向」の立場から、特定のリリースに依存しないように、これらのメソッドを温存するという戦略も考えられます。□

テストケースを俯瞰する(3)

既存のアプリケーションに組み込む前に、それとは独立した環境下のテストケースで、新規のモジュールの動作を確認します。



既存のモジュール hexagon.py を再利用しながら、新たなモジュールの動作を検証するために、テストケースを作成します。

テストケース3:テトリミノの動きの制約

テストケースを起動すると、ウィンドウが開きます。テトリミノを左右にシフト/回転させて、境界に達すると、内側に押し戻されます。

シフト
テトリミノが「左」の境界に達すると、その先には移動できません。 テトリミノは境界内を自由に移動できます。 テトリミノが「右」の境界に達すると、その先には移動できません。
回転
1)テトリミノは左の境界には達していません。 2)しかし、そのまま回転させようとすると、境界からはみ出します。 3)すると、境界の内側(1つ右側)まで押し戻されます。

《Note》2) の図は、コードの断片をコメントにしてから、テトリミノを回転したときに(説明のためにあえて)update しないようにして作成したものです。□