
Magic:the Gathering (M:tG)のプログラミングに関する投稿を始めたいと思います。そして、「マナ」の概念で、最も単純なことで後悔し始めます。 マナはすべての呪文が支払うものです。 マナには5種類しかありませんが、実際にはすべてが少し複雑です。 それを理解してみましょう。
まず、5種類のマナはありません。 「デュアル」マナを捨てたとしても(どちらかを支払うことができる場合)、アーティファクトが購入され、多くの(ほとんどの)呪文のコストで現れる「無色」マナが残っています。 また、支払いについてではなく「価格」について話している場合、マナXが表示されます。 価格がルールによって規制されている状況。
いくつかの例を見てみましょう。
普通の色

class Mana
{
⋮
public int Blue { get; set; }
public bool IsBlue { get { return Blue > 0; } }
//
}
プール内のマナのコストと可用性を1つのエンティティに含めることができるという考えで遊んでいる間、もう少し夢を見ることができます。 たとえば、テキスト文字列でマナ表現を取得するにはどうすればよいですか(Sliver LegionのWUBRGなど)。 このようなもの:
public string ShortString
{
get
{
StringBuilder sb = new StringBuilder();
if (Colorless > 0) sb.Append(Colorless);
if (Red > 0) sb.Append( 'R' .Repeat(Red));
if (Green > 0) sb.Append( 'G' .Repeat(Green));
if (Blue > 0) sb.Append( 'U' .Repeat(Blue));
if (White > 0) sb.Append( 'W' .Repeat(White));
if (Black > 0) sb.Append( 'B' .Repeat(Black));
if (HasX) sb.Append( "X" );
return sb.ToString();
}
}
これが、モデルの弱点を説明する方法です。 デュアルマナがあることを知らなかった場合(そして何かを知っている場合)、その後の変更によって、本質と相互作用するすべての要素にアーキテクチャ上の黙示録が生じます。 これが最初です。
第二に、同じことを5回以上書くのは悪いことです。 プールから特定のマナコストを支払う方法を実装しているとします。 同じアプローチに従う場合は、おそらく次のようなものを書く必要があります。
public void PayFor(Mana cost)
{
if (cost.Red > 0) Red -= cost.Red;
if (cost.Blue > 0) Blue -= cost.Blue;
if (cost.Green > 0) Green -= cost.Green;
if (cost.Black > 0) Black -= cost.Black;
if (cost.White > 0) White -= cost.White;
int remaining = cost.Colorless;
while (remaining > 0)
{
if (Red > 0) { --Red; --remaining; continue ; }
if (Blue > 0) { --Blue; --remaining; continue ; }
if (Black > 0) { --Black; --remaining; continue ; }
if (Green > 0) { --Green; --remaining; continue ; }
if (White > 0) { --White; --remaining; continue ; }
if (Colorless > 0) { --Colorless; --remaining; continue ; }
Debug.Fail( "Should not be here" );
}
}
繰り返しの回数は「ロールオーバー」しませんが、確かに迷惑です。 C#にはマクロがないことを思い出させてください。
無色マナ

public int ConvertedManaCost
{
get
{
return Red + Blue + Green + Black + White + Colorless;
}
}
もっと深刻なものが必要な場合は、マナの量が特定のコストを満たしているかどうかを計算できます。
public bool EnoughToPay(Mana cost)
{
if (Red < cost.Red || Green < cost.Green || White < cost.White ||
Blue < cost.Blue || Black < cost.Black)
return false ;
// can we pay the colourless price?
return ((Red - cost.Red) + (Green - cost.Green) + (White - cost.White) +
(Blue - cost.Blue) + (Black - cost.Black) + Colorless) >= cost.Colorless;
}
無色マナは、色マナとは異なり、決定性の度合いを低下させます。 たとえば、1枚のカードにRGを支払った場合、自動的に呪文をプレイすることはできません。 どの色のマナを支払う必要があるかは明確ではありません。
ハイブリッドマナ

2RG
行を取得して予測可能に
2RG
Mana
ようなオブジェクトを取得できると考えました。 そしてここでも-新たなルール、メカニズム自体だけでなく、メモも。 結局のところ、ダブルマナシンボルのつづり方は? ほとんどの場合:
{WB}{WB}{WB}
。 Vootoot、そしてこのためにはすでにパーサーが必要です。
さらに、マナがスリーブラーのように繁殖したと想像してください-紫、紫など。 上記で引用したコードに新しいマナのサポートを追加するのは簡単ですか? そうです-それは非現実的に難しいです。 別のアプローチが必要です。
この非常に「異なるアプローチ」を見る前に、コストと支払いは2つの異なるものであることに言及する必要があります。 より正確には、それらは似ていますが、たとえば、コストに
X
アイコンが含まれる場合があります。 たとえば、
XG
を支払い、
X
ライフを獲得します。 本質を生み出さないために、私は
Mana
の本質が一つであるべきだとまだ信じています。
X
の状況は単独で解決できます(
public bool HasX
)、または少し一般化できます。したがって、突然
XY
のコストのカードが表示された場合、ロジック全体を書き換える必要はありません。 さらに、特定の色のマナでのみ特定の
X
支払うことができる状況があります。 これも考慮に入れる必要があります。
メタプログラミングについて
少なくとも過剰なコードの重複を回避し、また、たとえば各クラスプロパティを書き換えずに(たとえば、個々のイベントを介して)Observableサポートを突然追加する必要がある場合にこのような場合から身を守るために、この問題ではメタプログラミングが必要であるように思えます。 C#はそのような目的には適していません(PostSharpがあることを考慮しても)。 つまり、次の目標を考慮できるものが必要です。
- 任意の色数のカラーマナを維持します。 つまり、たとえば、 ライラックマナを追加してもシステムが破損することはなく、コードにわずかな変更しか加えられません。
- ハイブリッドマナを維持します。 これ以上マナは構築されず、それ以上になると思うので、既存のすべての色に対してデュアルマナをサポートすることができます。 さらに、新しいタイプの「プライマリ」マナを追加する場合、その「ハイブリダイゼーション」のための追加のアクションは必要ありません。
- 無色のマナとその操作を正しく維持します。
- 標準のマナ表記、つまり 文字列から
Mana
型のオブジェクトを収集できるパーサーがあります。
それでは、メタプログラミングをサポートする言語で上記のすべてのプロパティを徐々に実装する方法を見てみましょう。 もちろん、私はブー語について話しています。 (まだNemerleがありますが、私はそれが苦手です。)
色は普通です(試行番号2)

「そう、単純なことから始めましょう」と書きたいのですが、悲しいかな、そうではありません。 まず、2つのプロジェクトを実行することから始めましょう。
-
MagicTheGathering.Entities
Mana
などのエンティティ)。 このアセンブリは、後でC#またはF#で記述された他のアセンブリとILMerge
できます。 -
MagicTheGathering.Meta
エンティティを「収集」するメタ抽象化のためのメタ。
フィールド
森林のサポートから始めましょう。
[ManaType( "Green" , "G" , "Forest" )]
class Mana:
public def constructor ():
pass
そのため、森林から始めて、マナクラスに異なる土地の属性を掛けます。 これらの属性から何が必要ですか? まず、適切なフィールドを追加する必要があります。 簡単です:
class ManaTypeAttribute(AbstractAstAttribute):
colorName as string
colorAbbreviation as string
landName as string
public def constructor (colorName as StringLiteralExpression,
colorAbbreviation as StringLiteralExpression, landName as StringLiteralExpression):
self .colorName = colorName.Value
self .colorAbbreviation = colorAbbreviation.Value
self .landName = landName.Value
public override def Apply(node as Node):
AddField(node)
private def AddField(node as Node):
c = node as ClassDefinition
f = [|
$(colorName.ToLower()) as Int32
|]
c.Members.Add(f)
したがって、元のエッセンスから呼び出される属性のコンストラクターを定義しました。 上記の例では、既存のクラスにフィールドを追加します。 これは3つのステップで実行されます。
- 最初に、属性が
ClassDefinition
型に適用される要素を提示します - 次に、スプライスを使用してフィールドを作成し(
colorName
文字列の内容を実際のフィールド名にcolorName
ます)、引用colorName
ます(角括弧[|
および|]
を使用して、コンストラクトをプロパティ要素に変換します - 収集したプロパティをクラスに追加します。
それでは、初期結果と最終結果を比較しましょう。
ブー | C# |
[ManaType( "Green" , "G" , "Forest" )]
| [Serializable]
|
直接および派生プロパティ
うーん、それは私たちが望んでいたことではありませんか? :)このフィールドに単純なプロパティを追加するのはどうですか? ワトソン小学校
AddField()
定義を変更するだけです:
private def AddField(node as Node):
c = node as ClassDefinition
r = ReferenceExpression(colorName)
f = [|
[Property($r)]
$(colorName.ToLower()) as Int32
|]
c.Members.Add(f)
次に、チェックフィールドを作成します。たとえば、カードが緑の場合は
IsGreen
が
true
返し、
true
ない場合は
IsGreen
返し
true
。 私たちはこのプロパティに再び会うでしょう、なぜなら 特にハイブリッドカードと相互作用します。 これを実装する最初の試みは次のとおりです。
private def AddIndicatorProperty(node as Node):
c = node as ClassDefinition
r = ReferenceExpression(colorName)
f = [|
$( "Is" + colorName) as bool:
get :
return ($r > 0);
|]
c.Members.Add(f)
派生プロパティの実装も非常に簡単でした。 そして、これがすべてC#に翻訳される様子です:
[Serializable]
public class Mana
{
// Fields
protected int green;
// Properties
public int Green
{
get
{
return this .green;
}
set
{
this .green = value ;
}
}
public bool IsGreen
{
get
{
return ( this .Green > 0);
}
}
}
相互作用
総コスト(変換されたマナコスト)を生成してみましょう。 これを行うには、無色マナを実装する必要がありますが、実際にはそれほど難しくありません。 しかし、すべてのマナ+無色の合計を自動生成する方法は? これを行うには、次のアプローチを使用します。
- 最初に、新しい属性を作成します。その中には、要約する必要があるプロパティのリストがあります
class ManaSumAttribute(AbstractAstAttribute):
static public LandTypes as List = []
⋮
- ここで、「土地プロパティ」を作成するときに、この静的プロパティに名前を書き込みます。
public def constructor (colorName as StringLiteralExpression,
⋮
ManaSumAttribute.LandTypes.Add( self .colorName)
- 次に、このプロパティを使用して合計を作成します。
class ManaSumAttribute(AbstractAstAttribute):
⋮
public override def Apply(node as Node):
c = node as ClassDefinition
root = [| Colorless |] as Expression
for i in range(LandTypes.Count):
root = BinaryExpression(BinaryOperatorType.Addition,
root, ReferenceExpression(LandTypes[i] as string))
p = [|
public ConvertedManaCost:
get :
return $root
|]
c.Members.Add(p)
それでは、山のサポートを追加して、
ConvertedManaCost
プロパティに対して何が出力されるかを確認しましょう。 取得するものは次のとおりです。
public int ConvertedManaCost
{
get
{
return (( this .Colorless + this .Green) + this .Red);
}
}
ご覧のとおり、すべてが機能します:)
ハイブリッド土地サポート
さて、何かが私たちのためにうまくいき始めました。 コードと歓声にすべての土地のサポートを追加し、Booはそれらの10個のプロパティを自動生成し、さらに合計を行います。 他に何が必要ですか? ハイブリッドランドのサポートについてはどうですか。 もちろん、これはもっと難しいです。なぜなら、 既存の土地のすべてのペアを取得する必要があります。 でも面白いので、試してみませんか?
原理は合計ジェネレーターの場合と同じです-静的フィールドを使用して、異なるマナの属性を入力します。 そして... その後、非常に複雑なこと。 要するに、すべての有効なマナペアを探しており、それらに対して、「同じタイプ」の通常のマナとほぼ同じプロパティを作成します。
class HybridManaAttribute(AbstractAstAttribute):
static public LandTypes as List = []
public override def Apply(node as Node):
mergedTypes as List = []
for i in range(LandTypes.Count):
for j in range(LandTypes.Count):
unless (mergedTypes.Contains(string.Concat(LandTypes[i], LandTypes[j])) or
mergedTypes.Contains(string.Concat(LandTypes[j], LandTypes[i])) or
i == j):
mergedTypes.Add(string.Concat(LandTypes[i], LandTypes[j]))
// each merged type becomes a field+property pair
c = node as ClassDefinition
for n in range(mergedTypes.Count):
name = mergedTypes[n] as string
r = ReferenceExpression(name)
f = [|
[Property($r)]
$(name.ToLower()) as int
|]
c.Members.Add(f)
多くのプロパティが取得されているため、結果は表示しません。 結局、それらを同種のマナの属性に保持することはできません。 当時、ハイブリッドマナについては何も知られていない。 それらを別の属性に転送しましょう。 したがって、ハイブリッドプロパティと単一プロパティの両方を使用して、カードの色を理解する必要があります。
class ManaIndicatorsAttribute(AbstractAstAttribute):
public override def Apply(node as Node):
c = node as ClassDefinition
for i in range(ManaSumAttribute.LandTypes.Count):
basic = ManaSumAttribute.LandTypes[i] as string
hybridLands as List = []
for j in range(HybridManaAttribute.HybridLandTypes.Count):
hybrid = HybridManaAttribute.HybridLandTypes[j] as string
if (hybrid.Contains(basic)):
hybridLands.Add(hybrid)
rbasic = ReferenceExpression(basic.ToLower())
b = Block();
b1 = [| return true if $rbasic > 0 |]
b.Statements.Add(b1)
for k in range(hybridLands.Count):
rhybrid = ReferenceExpression((hybridLands[k] as string).ToLower())
b2 = [| return true if $rhybrid > 0 |]
b.Statements.Add(b2)
r = [|
$( "Is" + basic):
get :
$b;
|]
c.Members.Add(r)
出来上がり! 上記のコードでは、このタイプが影響するすべてのタイプのマナを見つけ、それらをゼロと比較します。 これは
IsXxx
プロパティを計算するのに最適な方法ではありませんが、機能しますが、Reflectorのレベルではこのような混乱はよくありません。
文字列表現とパーサー
マナの単純なタイプごとに、文字列表現があります。 この表現により、文字列の読み取りと取得の両方が可能になります。 シンプルなものから始めましょう
ToString()
介して発行するマナの文字列表現を取得します:
class ManaStringAttribute(AbstractAstAttribute):
public override def Apply(node as Node):
b = Block()
b1 = [|
sb.Append(colorless) if colorless > 0
|]
b.Statements.Add(b1)
for i in range(ManaTypeAttribute.LandTypes.Count):
land = ReferenceExpression((ManaTypeAttribute.LandTypes[i] as string ).ToLower())
abbr = StringLiteralExpression(ManaTypeAttribute.LandAbbreviations[i] as string )
b2 = [|
sb.Append($abbr) if $land > 0;
|]
b.Statements.Add(b2)
for j in range(HybridManaAttribute.HybridLandTypes.Count):
land = ReferenceExpression((HybridManaAttribute.HybridLandTypes[j] as string ).ToLower())
abbr = StringLiteralExpression( "{" +
(HybridManaAttribute.HybridLandAbbreviations[j] as string ) + "}" )
b3 = [|
sb.Append($abbr) if $land > 0;
|]
b.Statements.Add(b3)
b3 = [|
sb.Append( "X" ) if hasX
|]
m = [|
public override def ToString():
sb = StringBuilder();
$b
return sb.ToString()
|]
c = node as ClassDefinition
c.Members.Add(m)
まあ、私たちはほとんどすべてを持っています、それは最も重要なものを追加することだけに残っています-マナ記述パーサー、すなわち プログラムが
2GG{RW}
行から対応するオブジェクトを作成できるようにします。 マナパーサーを3つの部分に分けましょう-基本マナ、ハイブリッドマナ、および「その他すべて」の分析の統計。 したがって、基本的なマナは解析するのが難しくありません:
// basic land cases are in a separate block
basicLandCases = Block()
for i in range(ManaTypeAttribute.LandTypes.Count):
name = ManaTypeAttribute.LandTypes[i] as string
abbr = ManaTypeAttribute.LandAbbreviations[i] as string
rAbbr = CharLiteralExpression( char .ToUpper(abbr[0]))
rName = ReferenceExpression(name)
case = [|
if ( char .ToUpper(spec[i]) == $rAbbr):
m.$rName = m.$rName + 1
continue
|]
basicLandCases.Statements.Add(case);
マナのスペルの順番(
RG
または
GR
)がパーサーに影響しないように、ハイブリッドマナをいじる必要があります。 ただし、ソリューションはそれほど複雑ではありません。
// hybrid land cases are in a much smarter block
hybridLandCases = Block()
for i in range(HybridManaAttribute.HybridLandTypes.Count):
name = HybridManaAttribute.HybridLandTypes[i] as string
abbr = HybridManaAttribute.HybridLandAbbreviations[i] as string
// build an appreviation literal
abbr1 = StringLiteralExpression(abbr)
abbr2 = StringLiteralExpression(abbr[1].ToString() + abbr[0].ToString())
case = [|
if (s == $abbr1 or s == $abbr2):
m.$name = m.$name + 1
continue
|]
hybridLandCases.Statements.Add(case)
それでは、メソッド自体を一連のケースとして作成できます。 カラーマナに加えて、無色のマナと
X
記号のサポートを追加します。
// the method itself
method = [|
public static def Parse(spec as string) as Mana:
sb = StringBuilder()
cb = StringBuilder() // composite builder
inHybrid = false // set when processing hybrid mana
m = Mana()
for i in range(spec.Length):
if (inHybrid):
cb.Append(spec[i])
continue
if ( char .IsDigit(spec[i])):
sb.Append(spec[i])
continue ;
if (spec[i] == '{' ):
inHybrid = true
continue
if (spec[i] == '}' ):
raise ArgumentException( "Closing } without opening" ) if not inHybrid
inHybrid = false
s = cb.ToString().ToUpper()
raise ArgumentException( "Only two-element hybrids supported" ) if s.Length != 2
$hybridLandCases
raise ArgumentException( "Hybrid mana " + s + " is not supported" )
$basicLandCases
if ( char .ToUpper(spec[i]) == 'X' ):
m.HasX = true
continue ;
|]
// add it
c = node as ClassDefinition
c.Members.Add(method)
このマクロの結果は表示しません。なぜなら、 多くのコードが生成されます。
おわりに

ああ、はい、私たちの本質は次のようになりました。
[ManaType( "Green" , "G" , "Forest" )]
[ManaType( "Red" , "R" , "Mountain" )]
[ManaType( "Blue" , "U" , "Island" )]
[ManaType( "Black" , "B" , "Swamp" )]
[ManaType( "White" , "W" , "Plains" )]
[ManaSum]
[HybridMana]
[ManaIndicators]
[ManaString]
[ManaParser]
class Mana:
[Property(Colorless)]
colorless as int
[Property(HasX)]
hasX as bool
それだけです。 コメントを歓迎します。 ■