プログラミングマジック:収集-§1マナ


Magic:the Gathering (M:tG)のプログラミングに関する投稿を始めたいと思います。そして、「マナ」の概念で、最も単純なことで後悔し始めます。 マナはすべての呪文が支払うものです。 マナには5種類しかありませんが、実際にはすべてが少し複雑です。 それを理解してみましょう。



まず、5種類のマナはありません。 「デュアル」マナを捨てたとしても(どちらかを支払うことができる場合)、アーティファクトが購入され、多くの(ほとんどの)呪文のコストで現れる「無色」マナが残っています。 また、支払いについてではなく「価格」について話している場合、マナXが表示されます。 価格がルールによって規制されている状況。

いくつかの例を見てみましょう。

普通の色


基本マナは5色で、好きなように混ぜることができます。 これは、マナを扱うクラスでは、これらの5色のモデルが表示されるべきであるという直接のヒントです。 モデルの各要素は、OOPの単純なアプリケーションで起こるように、さらにいくつかを引き出します。 たとえば、マナを実装する単純なアプローチは次のようになります。



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#にはマクロがないことを思い出させてください。

無色マナ


無色のマナは、各タイプのマナがドメイン固有のロジックを描く最初のヒントであり、原則として予測が困難です。 たとえば、右側のマップは、M:tGのような柔軟性のないドメインで作業する際の優しい驚きの典型的な例です。 それでも、同じモデル(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があることを考慮しても)。 つまり、次の目標を考慮できるものが必要です。


それでは、メタプログラミングをサポートする言語で上記のすべてのプロパティを徐々に実装する方法を見てみましょう。 もちろん、私はブー語について話しています。 (まだNemerleがありますが、私はそれが苦手です。)

色は普通です(試行番号2)


Nbは、Boo(私たちが書いたもの)とC#(Reflectorがこれで見たもの)の2つの言語で一度に計算されます。 これは、マクロとメタメソッドの動作を説明するために行われました。 Boo自体は、ご想像のとおり、この点に関して透明ではありません。

「そう、単純なことから始めましょう」と書きたいのですが、悲しいかな、そうではありません。 まず、2つのプロジェクトを実行することから始めましょう。


フィールド


森林のサポートから始めましょう。



[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つのステップで実行されます。


それでは、初期結果と最終結果を比較しましょう。

ブーC#
[ManaType( "Green" , "G" , "Forest" )]
class Mana:
public def constructor ():
pass

[Serializable]
public class Mana
{
// Fields
protected int green;
}


直接および派生プロパティ


うーん、それは私たちが望んでいたことではありませんか? :)このフィールドに単純なプロパティを追加するのはどうですか? ワトソン小学校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)

次に、チェックフィールドを作成します。たとえば、カードが緑の場合はIsGreentrue返し、 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);
}
}
}

相互作用


総コスト(変換されたマナコスト)を生成してみましょう。 これを行うには、無色マナを実装する必要がありますが、実際にはそれほど難しくありません。 しかし、すべてのマナ+無色の合計を自動生成する方法は? これを行うには、次のアプローチを使用します。

  1. 最初に、新しい属性を作成します。その中には、要約する必要があるプロパティのリストがあります
    class ManaSumAttribute(AbstractAstAttribute):
    static public LandTypes as List = []


  2. ここで、「土地プロパティ」を作成するときに、この静的プロパティに名前を書き込みます。
    public def constructor (colorName as StringLiteralExpression,

    ManaSumAttribute.LandTypes.Add( self .colorName)

  3. 次に、このプロパティを使用して合計を作成します。
    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)

このマクロの結果は表示しません。なぜなら、 多くのコードが生成されます。

おわりに


特定の呪文のマナで支払うなど、いくつかのケースを解決しなかったという事実にもかかわらず、おそらく中断されます-Firefoxがすでにテキストボックスの文字数から落ち始めているからです。 この投稿で、拡張可能なエンティティを作成するのがいかに難しいか、そしてメタプログラミングがオプションではない場合があることを説明したいと思います。 ちなみに、完全なコード(この段階でその正確性を保証することはできません)は、 ここにあります 。 ブーは冷酷です。

ああ、はい、私たちの本質は次のようになりました。



[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

それだけです。 コメントを歓迎します。 ■

Source: https://habr.com/ru/post/J72721/


All Articles