我的任务是实现一个游戏。我任务的一部分是实现怪物的行为。在任务中,怪物有不同的攻击方式,每次攻击都有一定的伤害率。此外,我必须为每个怪物随机生成一个攻击。
龙可以命中(伤害:5)和喷火(伤害:20)
蜘蛛可以命中(伤害:5)和咬(伤害:8)。
我的想法是创建一个抽象的Monster类,并让类Spider和Dragon扩展这个类
然后我将创建一个名为Attack的接口,并使用方法Attack创建子类Hit、Fire和Bite,它们将此接口作为策略来实现。我还创建了两个生成器方法,一个用于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()
,它返回一个以damage
number
为属性的对象,而不仅仅是返回number
。我将在";组合攻击";部分
interface AttackData {
damage: number;
name: string;
}
interface AttackStrategy {
attack(): AttackData;
}
在这个版本中,一个Monster
用其doAttack()
方法中的number
调用另一Monster
的takeDamage()
方法。
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()
,它执行攻击并返回伤害量。
Hit
、Bite
和Fire
现在都是相同的。它们要么需要更多的区别,要么需要是通用Attack
类的实例。我们仍然可以通过向构造函数传递不同的参数(如message: string
、name: 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");
单个类更灵活,因为我们可以创建无限的攻击。
组合攻击
你的attackSpiderGenerator
和attackDragonGenerator
不允许一个怪物实例在攻击之间切换。构造函数随机选择一个,这就是该实例的攻击。
我们希望创建一个帮助器,它可以组合攻击,同时仍然符合与单个攻击相同的接口。
如果我们想知道被调用的攻击的name
,那么我们的方法attack(): number
是不够的,因为组合攻击的名称各不相同。所以让我们稍微改变一下接口。我们定义了一个包含name
和damage
属性的AttackData
。CCD_ 39具有返回CCD_ 41的函数CCD_。
interface AttackData {
damage: number;
name: string;
}
interface AttackStrategy {
attack(): AttackData;
}
我让AttackSwitcher
constructor
采用可变数量的自变量,其中每个自变量要么是AttackStrategy
,要么是频率的AttackStrategy
和weight
的元组。每次攻击的默认权重为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();
}
}
制造怪物
所有蜘蛛在相同的攻击中都有相同的重量和相同的伤害值吗?这些选择由你自己决定。
这个Spider
和Dragon
根本不需要是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!" );
}
不如我们允许一队蜘蛛与一条龙对抗?使用与组合攻击相同的方法,我们定义了一个由单个Monster
和BattleTeam
怪物共享的接口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!");
}
完整代码
打字游戏场-点击";运行";看看谁赢了!