如果某个子类不能使用某些策略,如何使用策略设计模式



我的任务是实现一个游戏。我任务的一部分是实现怪物的行为。在任务中,怪物有不同的攻击方式,每次攻击都有一定的伤害率。此外,我必须为每个怪物随机生成一个攻击。

可以命中(伤害:5)和喷火(伤害:20)
蜘蛛可以命中(伤害:5)和咬(伤害:8)。

我的想法是创建一个抽象的Monster类,并让类Spider和Dragon扩展这个类
然后我将创建一个名为Attack的接口,并使用方法Attack创建子类HitFireBite,它们将此接口作为策略来实现。我还创建了两个生成器方法,一个用于Dragon,另一个用于Spider,但我认为这不好,也许有人知道更好的方法。

abstract class Monster {
private health = 200;
attack: Attack;
constructor(attack: Attack) {
this.attack = attack;
}
getAttackDamage() {
return this.attack.attack();
}
getHealth() {
return this.health;
}
damage(damage: number) {
let isDefeated = false;
this.health -= damage;
if (this.health <= 0) {
isDefeated = true;
}
return isDefeated;
}
}
class Dragon extends Monster {
constructor() {
super(attackDragonGenerator());
}
setAttack() {
this.attack = attackDragonGenerator();
}
}
class Spider extends Monster {
constructor() {
super(attackSpiderGenerator());
}
setAttack() {
this.attack = attackSpiderGenerator();
}
}
interface Attack {
attack: () => number;
damage: number;
}
class Hit implements Attack {
damage;
constructor(damage: number) {
this.damage = damage;
}
attack() {
return this.damage;
}
}
class Bite implements Attack {
damage;
constructor(damage: number) {
this.damage = damage;
}
attack() {
return this.damage;
}
}
class Fire implements Attack {
damage;
constructor(damage: number) {
this.damage = damage;
}
attack() {
return this.damage;
}
}
const attacksSpider: Attack[] = [new Hit(5), new Bite(8)];
const attacksDragon: Attack[] = [new Hit(5), new Fire(20)];
const attackSpiderGenerator = () => {
const index = randomIntFromInterval(0, 1);
return attacksSpider[index];
};
const attackDragonGenerator = () => {
const index = randomIntFromInterval(0, 1);
return attacksDragon[index];
};

怪物之间的互动

显然,您所拥有的有效,但还有改进的空间。有很多方法可以让你把这些碎片组合在一起。现在你的Monster可以攻击,返回number,可以接收damage,获得number。我建议其中一种方法应该与其他对象交互,而不是与数字交互。

现在,我将把AttackStrategy定义为方法attack(),它返回一个以damagenumber为属性的对象,而不仅仅是返回number。我将在";组合攻击";部分

interface AttackData {
damage: number;
name: string;
}
interface AttackStrategy {
attack(): AttackData;
}

在这个版本中,一个Monster用其doAttack()方法中的number调用另一MonstertakeDamage()方法。

interface CanAttack {
attack(target: CanTakeDamage): void;
}
interface CanTakeDamage {
takeDamage(damage: number): void;
}
class Monster implements CanTakeDamage, CanAttack {
constructor(
public readonly name: string,
private strategy: AttackStrategy,
private _health = 200
) { }
attack(target: CanTakeDamage): void {
const attack = this.strategy.attack();
target.takeDamage(attack.damage);
console.log( `${this.name} used ${attack.name}` );
}
takeDamage(damage: number): void {
// don't allow negative health
this._health = Math.max(this._health - damage, 0);
}
get health(): number {
return this._health;
}
get isDefeated(): boolean {
return this._health === 0;
}
}

相反的方法是takeDamage()方法接收攻击Monster作为自变量,doAttack()返回伤害number

我认为这在这种情况下没有多大意义。但希望它能说明一点。不一定有";错误的";把它拼凑在一起的方法,但有些方法感觉更合乎逻辑和自然。那就去吧!我不喜欢这个,因为目标负责调用攻击者的doAttack()方法,这感觉是倒退的。

class Monster implements CanTakeDamage, CanAttack {
constructor(
public readonly name: string,
private strategy: AttackStrategy,
private _health = 200
) { }
attack(): AttackData {
const attack = this.strategy.attack();
console.log(`${this.name} used ${attack.name}`);
return attack;
}
takeDamage(attacker: CanAttack): void {
const { damage } = attacker.attack();
// don't allow negative health
this._health = Math.max(this._health - damage, 0);
}
get health(): number {
return this._health;
}
get isDefeated(): boolean {
return this._health === 0;
}
}

创建攻击

我将从interface Attack中删除属性damage,并将其视为实现细节。如果我们想要一个具有多种攻击类型和不同伤害值的Monster,这一点将非常重要。现在Attack只有一个方法attack(),它执行攻击并返回伤害量。

HitBiteFire现在都是相同的。它们要么需要更多的区别,要么需要是通用Attack类的实例。我们仍然可以通过向构造函数传递不同的参数(如message: stringname: string等)来支持通用Attack实例的一些差异。

独立类

interface Attack {
attack(): number;
}
class BaseAttack implements Attack {
constructor(public readonly damage: number) { }
protected message(): string {
return "You've been attacked!";
}
attack(): number {
console.log(this.message());
return this.damage;
}
}
class Hit extends BaseAttack {
protected message(): string {
return `POW! Strength ${this.damage} punch incoming!`;
}
}
class Bite extends BaseAttack {
protected message(): string {
return "Chomp!";
}
}
class Fire extends BaseAttack {
protected message(): string {
return "Burn baby, burn!";
}
}

单类

class Attack implements AttackStrategy {
constructor(private damage: number, private name: string) { }
attack(): AttackData {
return {
name: this.name,
damage: this.damage
}
}
}
const attack1 = new Attack(10, "Chomp!");
const attack2 = new Attack(5, "Slap");

单个类更灵活,因为我们可以创建无限的攻击。


组合攻击

你的attackSpiderGeneratorattackDragonGenerator不允许一个怪物实例在攻击之间切换。构造函数随机选择一个,这就是该实例的攻击。

我们希望创建一个帮助器,它可以组合攻击,同时仍然符合与单个攻击相同的接口。

如果我们想知道被调用的攻击的name,那么我们的方法attack(): number是不够的,因为组合攻击的名称各不相同。所以让我们稍微改变一下接口。我们定义了一个包含namedamage属性的AttackData。CCD_ 39具有返回CCD_ 41的函数CCD_。

interface AttackData {
damage: number;
name: string;
}
interface AttackStrategy {
attack(): AttackData;
}

我让AttackSwitcherconstructor采用可变数量的自变量,其中每个自变量要么是AttackStrategy,要么是频率的AttackStrategyweight的元组。每次攻击的默认权重为1。我们将对这些值进行归一化,以便选择具有正确概率的随机攻击。

type AttackArg = AttackStrategy | [AttackStrategy, number];
class AttackSwitcher implements AttackStrategy {
private attacks: [AttackStrategy, number][];
// must have at least one arg
constructor(...args: [AttackArg, ...AttackArg[]]) {
// default weight is 1 per attack if not assigned
const tuples = args.map<[AttackStrategy, number]>((arg) =>
Array.isArray(arg) ? arg : [arg, 1]
);
// normalize so that the sum of all weights is 1
const sum = tuples.reduce((total, [_, weight]) => total + weight, 0);
this.attacks = tuples.map(([attack, weight]) => [attack, weight / sum]);
}
private getRandomAttack(): AttackStrategy {
// compare a random number to the rolling sum of weights
const num = Math.random();
let sum = 0;
for (let i = 0; i < this.attacks.length; i++) {
const [attack, weight] = this.attacks[i];
sum += weight;
if (sum >= num) {
return attack;
}
}
// should not be here except due to rounding errors
console.warn("check your math");
return this.attacks[0][0];
}
attack(): AttackData {
return this.getRandomAttack().attack();
}
}

制造怪物

所有蜘蛛在相同的攻击中都有相同的重量和相同的伤害值吗?这些选择由你自己决定。

这个SpiderDragon根本不需要是class,因为我们真正要做的只是用特定的args构建Monster的特定实例。

class Spider extends Monster {
constructor(name: string = "Spider") {
super(
name,
new AttackSwitcher(
// 5:1 ratio of Bite to Hit
new Attack(5, "8-Legged Slap"),
[new Attack(8, "Spider Bite"), 5]
),
// 100 base health
100
);
}
}
class Dragon extends Monster {
constructor(name: string = "Dragon") {
super(
name,
new AttackSwitcher(
// equal incidence of both attacks
new Attack(5, "Tail Whip"),
new Attack(20, "Fire  Breath")
)
);
}
}

怪物团队

我们的怪物并不势均力敌,所以我不得不给Spider更多的攻击机会,以获得战斗结果的任何变化。我们称之为spider.attack(dragon)dragon.attack(spider)

function testAttacks() {
const spider = new Spider();
const dragon = new Dragon();
let i = 0;
while (! spider.isDefeated && ! dragon.isDefeated ) {
if ( i % 5 ) {
spider.attack(dragon);
console.log(`dragon health: ${dragon.health}`);
} else {
dragon.attack(spider);
console.log(`spider health: ${spider.health}`);
}
i++;
}
console.log( spider.isDefeated ? "DRAGON WINS!" : "SPIDER WINS!" );
}

不如我们允许一队蜘蛛与一条龙对抗?使用与组合攻击相同的方法,我们定义了一个由单个MonsterBattleTeam怪物共享的接口CanBattle

interface CanBattle extends CanAttack, CanTakeDamage {
health: number;
isDefeated: boolean;
name: string;
}
class BattleTeam implements CanBattle {
private monsters: CanBattle[];
private currentIndex: number;
// must have at least one monster
constructor(
public readonly name: string,
...monsters: [CanBattle, ...CanBattle[]]
) {
this.monsters = monsters;
this.currentIndex = 0;
}
// total health for all monsters
get health(): number {
return this.monsters.reduce(
(total, monster) => total + monster.health
, 0);
}
// true if all monsters are defeated
get isDefeated(): boolean {
return this.health === 0;
}
// the current attacker/defender
get current(): CanBattle {
return this.monsters[this.currentIndex];
}
// damage applies to the current monster only
takeDamage(damage: number): void {
this.current.takeDamage(damage);
// maybe move on to the next monster
if (this.current.isDefeated) {
console.log(`${this.current.name} knocked out`);
if (this.currentIndex + 1 < this.monsters.length) {
this.currentIndex++;
console.log(`${this.current.name} up next`);
}
}
}
// current monster does the attack
attack(target: CanTakeDamage): void {
this.current.attack(target);
}
}

击球

一场战斗需要两个CanBattle物体轮流攻击对方,直到其中一个被击败。这一系列的攻击是一次迭代,所以我们可以使用迭代器协议。

战斗可以被视为迭代器,在每次迭代中都会发生来自交替方的攻击

class Battle implements Iterator<CanBattle, CanBattle>, Iterable<CanBattle> {
private leftIsAttacker: boolean = true;
private _winner: CanBattle | undefined;
constructor(public readonly left: CanBattle, public readonly right: CanBattle) { }
// returns the target of the current attack as `value`
public next(): IteratorResult<CanBattle, CanBattle> {
const attacker = this.leftIsAttacker ? this.left : this.right;
const target = this.leftIsAttacker ? this.right : this.left;
if (!this.isCompleted) {
attacker.attack(target);
this.leftIsAttacker = !this.leftIsAttacker;
}
if (target.isDefeated) {
this._winner = attacker;
}
return {
done: this.isCompleted,
value: target,
}
}
[Symbol.iterator]() {
return this;
}
get winner(): CanBattle | undefined {
return this._winner;
}
get isCompleted(): boolean {
return this._winner !== undefined;
}
}
function testBattle() {
const dragon = new Dragon("Dragon");
const spiderTeam = new BattleTeam(
"Spider Team",
// @ts-ignore warning about needed at least 1
...[1, 2, 3, 4].map(n => new Spider(`Spider ${n}`))
)
const battle = new Battle(dragon, spiderTeam);
for (let target of battle) {
console.log(`${target.name} health: ${target.health}`);
}
console.log(spiderTeam.isDefeated ? "DRAGON WINS!" : "SPIDER WINS!");
}

完整代码

打字游戏场-点击";运行";看看谁赢了!

最新更新