diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4eb4a3d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${file}" + } + ] +} \ No newline at end of file diff --git a/rpgsaga/src/effects/BurnEffect.ts b/rpgsaga/src/effects/BurnEffect.ts new file mode 100644 index 0000000..c4f9173 --- /dev/null +++ b/rpgsaga/src/effects/BurnEffect.ts @@ -0,0 +1,35 @@ +import { type IEffect, EffectType } from './IEffect'; +import { Hero } from '../models/Hero'; +import { Logger } from '../logging/Logger'; + +export class BurnEffect implements IEffect { + readonly type = EffectType.Burn; + readonly name = 'Горение'; + remainingTurns: number; + + constructor( + readonly damagePerTurn: number, + readonly duration: number + ) { + this.remainingTurns = duration; + } + + apply(target: Hero): void { + Logger.log(`${target.toString()} загорается! Будет получать ${this.damagePerTurn} урона каждый ход в течение ${this.duration} ходов`); + } + + onTurnEnd(target: Hero): void { + if (this.remainingTurns > 0) { + target.takeDamage(this.damagePerTurn); + // this.remainingTurns--; + Logger.log(`${target.toString()} горит и теряет ${this.damagePerTurn} здоровья`); + } + } + + merge(effect: IEffect): void { + if (effect.type === this.type) { + this.remainingTurns = Math.min(5, Math.max(this.remainingTurns, effect.remainingTurns)); + Logger.log(`Эффект горения усилен! Урон: ${this.damagePerTurn}, осталось ходов: ${this.remainingTurns}`); + } + } +} \ No newline at end of file diff --git a/rpgsaga/src/effects/EffectManager.ts b/rpgsaga/src/effects/EffectManager.ts new file mode 100644 index 0000000..4eade77 --- /dev/null +++ b/rpgsaga/src/effects/EffectManager.ts @@ -0,0 +1,75 @@ +import { type IEffect, EffectType } from './IEffect'; +import { Hero } from '../models/Hero'; +import { Logger } from '../logging/Logger'; + +export class EffectManager { + private static instance: EffectManager; + private _globalEffects: Map = new Map(); + + private constructor() {} + + static getInstance(): EffectManager { + if (!EffectManager.instance) { + EffectManager.instance = new EffectManager(); + } + return EffectManager.instance; + } + + applyEffect(target: Hero, effect: IEffect): void { + if (this.hasImmunity(target, effect.type)) { + Logger.log(`${target.toString()} иммунен к ${effect.name}`); + return; + } + + target.applyEffect(effect); + this.trackGlobalEffect(target.name, effect); + } + + private hasImmunity(target: Hero, effectType: EffectType): boolean { + if (target.constructor.name === 'Mage') { + return effectType === EffectType.Freeze || effectType === EffectType.Poison; + } + + if (target.constructor.name === 'Knight') { + return effectType === EffectType.Freeze; + } + + return false; + } + + private trackGlobalEffect(heroName: string, effect: IEffect): void { + if (!this._globalEffects.has(heroName)) { + this._globalEffects.set(heroName, []); + } + this._globalEffects.get(heroName)!.push(effect); + } + + processAllEffects(heroes: Hero[]): void { + for (const hero of heroes) { + if (hero.isAlive) { + hero.processEffects(); + } + } + } + + getActiveEffects(hero: Hero): IEffect[] { + return [...hero.activeEffects]; + } + + clearAllEffects(hero: Hero): void { + hero.removeEffectsByType(EffectType.Burn); + hero.removeEffectsByType(EffectType.Freeze); + hero.removeEffectsByType(EffectType.Poison); + this._globalEffects.delete(hero.name); + } + + getStatistics(): Map { + const stats = new Map(); + + for (const [heroName, effects] of this._globalEffects.entries()) { + stats.set(heroName, effects.length); + } + + return stats; + } +} \ No newline at end of file diff --git a/rpgsaga/src/effects/FreezeEffect.ts b/rpgsaga/src/effects/FreezeEffect.ts new file mode 100644 index 0000000..dcc8f23 --- /dev/null +++ b/rpgsaga/src/effects/FreezeEffect.ts @@ -0,0 +1,35 @@ +import { type IEffect, EffectType } from './IEffect'; +import { Hero } from '../models/Hero'; +import { Logger } from '../logging/Logger'; + +export class FreezeEffect implements IEffect { + readonly type = EffectType.Freeze; + readonly name = 'Заморозка'; + remainingTurns: number; + + constructor( + readonly damagePerTurn: number, + readonly duration: number + ) { + this.remainingTurns = duration; + } + + apply(target: Hero): void { + Logger.log(`${target.toString()} заморожен! Будет получать ${this.damagePerTurn} урона каждый ход в течение ${this.duration} ходов`); + } + + onTurnEnd(target: Hero): void { + if (this.remainingTurns > 0) { + target.takeDamage(this.damagePerTurn); + // this.remainingTurns--; + Logger.log(`${target.toString()} заморожен и теряет ${this.damagePerTurn} здоровья`); + } + } + + merge(effect: IEffect): void { + if (effect.type === this.type) { + const newDamage = this.damagePerTurn + effect.damagePerTurn; + Logger.log(`Эффект заморозки суммируется! Урон увеличен с ${this.damagePerTurn} до ${newDamage}`); + } + } +} \ No newline at end of file diff --git a/rpgsaga/src/effects/IEffect.ts b/rpgsaga/src/effects/IEffect.ts new file mode 100644 index 0000000..92a00fd --- /dev/null +++ b/rpgsaga/src/effects/IEffect.ts @@ -0,0 +1,19 @@ +import type { Hero } from "../models/Hero"; + +export enum EffectType { + Burn = 'burn', + Freeze = 'freeze', + Poison = 'poison' +} + +export interface IEffect { + readonly type: EffectType; + readonly name: string; + readonly duration: number; + readonly damagePerTurn: number; + remainingTurns: number; + + apply(target: Hero): void; + onTurnEnd(target: Hero): void; + merge(effect: IEffect): void; +} \ No newline at end of file diff --git a/rpgsaga/src/effects/PoisonEffect.ts b/rpgsaga/src/effects/PoisonEffect.ts new file mode 100644 index 0000000..936605a --- /dev/null +++ b/rpgsaga/src/effects/PoisonEffect.ts @@ -0,0 +1,35 @@ +import { type IEffect, EffectType } from './IEffect'; +import { Hero } from '../models/Hero'; +import { Logger } from '../logging/Logger'; + +export class PoisonEffect implements IEffect { + readonly type = EffectType.Poison; + readonly name = 'Отравление'; + remainingTurns: number; + + constructor( + readonly damagePerTurn: number, + readonly duration: number + ) { + this.remainingTurns = duration; + } + + apply(target: Hero): void { + Logger.log(`${target.toString()} отравлен! Будет получать ${this.damagePerTurn} урона каждый ход в течение ${this.duration} ходов`); + } + + onTurnEnd(target: Hero): void { + if (this.remainingTurns > 0) { + target.takeDamage(this.damagePerTurn); + // this.remainingTurns--; + Logger.log(`${target.toString()} страдает от отравления и теряет ${this.damagePerTurn} здоровья`); + } + } + + merge(effect: IEffect): void { + if (effect.type === this.type && effect instanceof PoisonEffect) { + this.remainingTurns = Math.min(5, this.remainingTurns + effect.remainingTurns); + Logger.log(`Эффект отравления продлён! Осталось ходов: ${this.remainingTurns}`); + } + } +} diff --git a/rpgsaga/src/factory/HeroFactory.ts b/rpgsaga/src/factory/HeroFactory.ts new file mode 100644 index 0000000..1e03c75 --- /dev/null +++ b/rpgsaga/src/factory/HeroFactory.ts @@ -0,0 +1,59 @@ +import { Hero } from '../models/Hero'; +import { Knight } from '../models/Knight'; +import { Archer } from '../models/Archer'; +import { Mage } from '../models/Mage'; + +export type HeroType = 'knight' | 'archer' | 'mage'; + +export interface HeroParams { + name: string; + health: number; + strength: number; +} + +export class HeroFactory { + private static readonly NAMES = [ + 'Артур', 'Эльдар', 'Гэндальф', 'Вильямс', 'Леголас', + 'Арагорн', 'Гимли', 'Фродо', 'Саруман', 'Боромир', + 'Турин', 'Беовульф', 'Сигурд', 'Роланд', 'Ланселот' + ]; + + static createHero(type: HeroType, params: HeroParams): Hero { + switch (type) { + case 'knight': + return new Knight(params.name, params.health, params.strength); + case 'archer': + return new Archer(params.name, params.health, params.strength); + case 'mage': + return new Mage(params.name, params.health, params.strength); + default: + throw new Error(`Unknown hero type: ${type}`); + } + } + + static createRandomHero(): Hero { + const types: HeroType[] = ['knight', 'archer', 'mage']; + const randomType = types[Math.floor(Math.random() * types.length)]; + const randomName = this.NAMES[Math.floor(Math.random() * this.NAMES.length)]; + const randomHealth = Math.floor(Math.random() * (200 - 80 + 1) + 80); + const randomStrength = Math.floor(Math.random() * (50 - 15 + 1) + 15); + + return this.createHero(randomType, { + name: randomName, + health: randomHealth, + strength: randomStrength + }); + } + + static createHeroesArray(count: number): Hero[] { + if (count % 2 !== 0) { + throw new Error('Количество игроков должно быть чётным'); + } + + const heroes: Hero[] = []; + for (let i = 0; i < count; i++) { + heroes.push(this.createRandomHero()); + } + return heroes; + } +} \ No newline at end of file diff --git a/rpgsaga/src/game/BattleManager.ts b/rpgsaga/src/game/BattleManager.ts new file mode 100644 index 0000000..4a3e914 --- /dev/null +++ b/rpgsaga/src/game/BattleManager.ts @@ -0,0 +1,123 @@ +import { Hero } from '../models/Hero'; +import { EffectManager } from '../effects/EffectManager'; +import { Logger } from '../logging/Logger'; + +export interface BattleResult { + winner: Hero; + loser: Hero; + turns: number; + damageDealt: number; + specialAbilitiesUsed: Map; +} + +export class BattleManager { + private effectManager = EffectManager.getInstance(); + private turnCount = 0; + private totalDamageDealt = 0; + private abilitiesUsed: Map = new Map(); + + async fight(hero1: Hero, hero2: Hero): Promise { + Logger.log(`\n НАЧАЛО БИТВЫ: ${hero1.toString()} vs ${hero2.toString()} \n`); + + this.turnCount = 0; + this.totalDamageDealt = 0; + this.abilitiesUsed.clear(); + + let attacker = this.determineFirstAttacker(hero1, hero2); + let defender = attacker === hero1 ? hero2 : hero1; + + while (hero1.isAlive && hero2.isAlive) { + this.executeTurn(attacker, defender); + this.turnCount++; + + [attacker, defender] = [defender, attacker]; + } + + const winner = hero1.isAlive ? hero1 : hero2; + const loser = winner === hero1 ? hero2 : hero1; + + Logger.log(`\n ПОБЕДИТЕЛЬ: ${winner.toString()} `); + + return { + winner, + loser, + turns: this.turnCount, + damageDealt: this.totalDamageDealt, + specialAbilitiesUsed: new Map(this.abilitiesUsed) + }; + } + + private determineFirstAttacker(hero1: Hero, hero2: Hero): Hero { + const hero1Chance = hero1.strength / (hero1.strength + hero2.strength); + const isHero1First = Math.random() < hero1Chance; + + Logger.log(`Определение первого атакующего: ${hero1.toString()} (${Math.round(hero1Chance * 100)}% шанс)`); + + return isHero1First ? hero1 : hero2; + } + + private executeTurn(attacker: Hero, defender: Hero): void { + this.effectManager.processAllEffects([attacker, defender]); + + if (!attacker.isAlive || !defender.isAlive) return; + + if (this.isHeroCharmed(attacker, defender)) { + Logger.log(`${attacker.toString()} заворожён и пропускает ход!`); + return; + } + + const useSpecial = this.shouldUseSpecialAbility(attacker); + const damageBefore = defender.health; + + if (useSpecial) { + attacker.useSpecialAbility(defender); + this.trackAbilityUsage(attacker); + } else { + const damage = attacker.calculateBaseDamage(); + Logger.log(`${attacker.toString()} наносит урон ${damage} ${defender.toString()}`); + defender.takeDamage(damage); + } + + const damageDealt = damageBefore - defender.health; + this.totalDamageDealt += damageDealt; + + this.effectManager.processAllEffects([defender]); + } + + private isHeroCharmed(attacker: Hero, defender: Hero): boolean { + if (attacker.constructor.name === 'Mage') { + const mage = attacker as any; + return mage.isCharmedBy && mage.isCharmedBy(defender); + } + return false; + } + + private shouldUseSpecialAbility(hero: Hero): boolean { + const probabilities: Record = { + 'Knight': 0.4, + 'Archer': 0.35, + 'Mage': 0.45 + }; + + const probability = probabilities[hero.constructor.name] || 0.3; + return Math.random() < probability; + } + + private trackAbilityUsage(hero: Hero): void { + const heroType = hero.constructor.name; + const currentCount = this.abilitiesUsed.get(heroType) || 0; + this.abilitiesUsed.set(heroType, currentCount + 1); + } + + getStatistics(): { + averageTurnsPerBattle: number; + totalBattles: number; + abilityUsageRate: Map; + } { + return { + averageTurnsPerBattle: 0, + totalBattles: 0, + abilityUsageRate: new Map() + }; + } +} \ No newline at end of file diff --git a/rpgsaga/src/game/GameEngine.ts b/rpgsaga/src/game/GameEngine.ts new file mode 100644 index 0000000..5e589ac --- /dev/null +++ b/rpgsaga/src/game/GameEngine.ts @@ -0,0 +1,91 @@ +import { Hero } from '../models/Hero'; +import { Logger } from '../logging/Logger'; + +export class GameEngine { + private heroes: Hero[]; + private round: number = 0; + + constructor(heroes: Hero[]) { + this.heroes = [...heroes]; + } + + start(): Hero | null { + Logger.log('========== НАЧАЛО ТУРНИРА =========='); + + while (this.heroes.length > 1) { + this.runRound(); + } + + const winner = this.heroes[0]; + Logger.log(`\n========== ПОБЕДИТЕЛЬ: ${winner.toString()}! ==========`); + + return winner; + } + + private runRound(): void { + this.round++; + Logger.log(`\n========== КОН ${this.round} ==========\n`); + + const shuffled = this.shuffleArray([...this.heroes]); + const pairs: [Hero, Hero][] = []; + + for (let i = 0; i < shuffled.length; i += 2) { + if (i + 1 < shuffled.length) { + pairs.push([shuffled[i], shuffled[i + 1]]); + } + } + + const winners: Hero[] = []; + + for (const [hero1, hero2] of pairs) { + Logger.log(`${hero1.toString()} vs ${hero2.toString()}\n`); + const winner = this.battle(hero1, hero2); + winners.push(winner); + } + + this.heroes = winners; + } + + private battle(hero1: Hero, hero2: Hero): Hero { + let currentAttacker = Math.random() < 0.5 ? hero1 : hero2; + let currentDefender = currentAttacker === hero1 ? hero2 : hero1; + + while (hero1.isAlive && hero2.isAlive) { + this.makeTurn(currentAttacker, currentDefender); + + if (!currentDefender.isAlive) { + Logger.log(`${currentDefender.toString()} погибает!\n`); + return currentAttacker; + } + + [currentAttacker, currentDefender] = [currentDefender, currentAttacker]; + } + + return hero1.isAlive ? hero1 : hero2; + } + + private makeTurn(attacker: Hero, defender: Hero): void { + attacker.processEffects(); + defender.processEffects(); + + if (!attacker.isAlive || !defender.isAlive) return; + + const useSpecial = Math.random() < 0.4; + + if (useSpecial) { + attacker.useSpecialAbility(defender); + } else { + const damage = attacker.calculateBaseDamage(); + Logger.log(`${attacker.toString()} наносит урон ${damage} ${defender.toString()}`); + defender.takeDamage(damage); + } + } + + private shuffleArray(array: T[]): T[] { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + } +} \ No newline at end of file diff --git a/rpgsaga/src/index.ts b/rpgsaga/src/index.ts index 60dba4d..940a207 100644 --- a/rpgsaga/src/index.ts +++ b/rpgsaga/src/index.ts @@ -1,18 +1,25 @@ -export function sum(a: number, b: number): number { - return a + b; -} - -// Example usage of the addNumbers function -console.log("Hello world"); +import { HeroFactory } from './factory/HeroFactory'; +import { GameEngine } from './game/GameEngine'; -// Adding two integers -const result1 = sum(5, 3); -console.log(`5 + 3 = ${result1}`); +function main() { + console.log('Добро пожаловать в Akvelon RPG SAGA!'); -// Adding two floating-point numbers -const result2 = sum(2.5, 3.7); -console.log(`2.5 + 3.7 = ${result2}`); + const playerCount = 4; + + try { + const heroes = HeroFactory.createHeroesArray(playerCount); + console.log('\nСозданные герои:'); + heroes.forEach(hero => { + console.log(`${hero.toString()} | Здоровье: ${hero.health} | Сила: ${hero.strength}`); + }); + + const game = new GameEngine(heroes); + const winner = game.start(); + + console.log(`\nЧемпион: ${winner?.toString()}`); + } catch (error) { + console.error(error); + } +} -// Adding an integer and a floating-point number -const result3 = sum(10, 4.2); -console.log(`10 + 4.2 = ${result3}`); +main(); \ No newline at end of file diff --git a/rpgsaga/src/logging/Logger.ts b/rpgsaga/src/logging/Logger.ts new file mode 100644 index 0000000..aa4c1cd --- /dev/null +++ b/rpgsaga/src/logging/Logger.ts @@ -0,0 +1,20 @@ +export class Logger { + private static logs: string[] = []; + private static readonly LOG_FILE = 'battle.log'; + + static log(message: string): void { + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] ${message}`; + this.logs.push(logMessage); + console.log(message); + } + + static getLogs(): string[] { + return [...this.logs]; + } + + static clear(): void { + this.logs = []; + } + +} \ No newline at end of file diff --git a/rpgsaga/src/models/Archer.ts b/rpgsaga/src/models/Archer.ts new file mode 100644 index 0000000..f420d4e --- /dev/null +++ b/rpgsaga/src/models/Archer.ts @@ -0,0 +1,58 @@ +import { Hero } from './Hero'; +import type { IEffect } from '../effects/IEffect'; +import { BurnEffect } from '../effects/BurnEffect'; +import { FreezeEffect } from '../effects/FreezeEffect'; +import { PoisonEffect } from '../effects/PoisonEffect'; +import { Logger } from '../logging/Logger'; + +export class Archer extends Hero { + private _fireArrowsUsed = false; + private _iceArrowsUsedCount = 0; + private _poisonArrowsUsedCount = 0; + private static readonly MAX_POISON_ARROWS = 3; + + constructor(name: string, health: number, strength: number) { + super(name, health, strength); + } + + calculateBaseDamage(): number { + let damage = this._strength; + + return damage; + } + + useSpecialAbility(target: Hero): void { + if (!this._fireArrowsUsed) { + this._fireArrowsUsed = true; + const burnEffect = new BurnEffect(2, 3); + target.applyEffect(burnEffect); + Logger.log(`${this.toString()} использует Огненные стрелы! ${target.toString()} загорается (2 урона, 3 хода)`); + return; + } + + if (this._iceArrowsUsedCount < 2) { + this._iceArrowsUsedCount++; + const freezeEffect = new FreezeEffect(3, 3); + target.applyEffect(freezeEffect); + Logger.log(`${this.toString()} использует Ледяные стрелы! ${target.toString()} заморожен (3 урона, 3 хода)`); + return; + } + + if (this._poisonArrowsUsedCount < Archer.MAX_POISON_ARROWS) { + this._poisonArrowsUsedCount++; + const poisonEffect = new PoisonEffect(4, 4); + target.applyEffect(poisonEffect); + Logger.log(`${this.toString()} использует Отравленные стрелы! ${target.toString()} отравлен (4 урона, 4 хода)`); + return; + } + Logger.log(`${this.toString()} не может использовать Отравленные стрелы (максимум ${Archer.MAX_POISON_ARROWS} раза)`); + + const damage = this.calculateBaseDamage(); + Logger.log(`${this.toString()} наносит урон ${damage} ${target.toString()}`); + target.takeDamage(damage); + } + + applyEffect(effect: IEffect): void { + super.applyEffect(effect); + } +} \ No newline at end of file diff --git a/rpgsaga/src/models/Hero.ts b/rpgsaga/src/models/Hero.ts new file mode 100644 index 0000000..5dc4063 --- /dev/null +++ b/rpgsaga/src/models/Hero.ts @@ -0,0 +1,80 @@ +import type { EffectType, IEffect } from '../effects/IEffect'; +import { Logger } from '../logging/Logger'; + +export abstract class Hero { + private _health: number; + private readonly _maxHealth: number; + protected _activeEffects: IEffect[] = []; + + constructor( + public readonly name: string, + initialHealth: number, + protected _strength: number + ) { + this._health = initialHealth; + this._maxHealth = initialHealth; + } + + get health(): number { return this._health; } + get maxHealth(): number { return this._maxHealth; } + get strength(): number { return this._strength; } + get isAlive(): boolean { return this._health > 0; } + get activeEffects(): readonly IEffect[] { return this._activeEffects; } + + takeDamage(damage: number): void { + const actualDamage = this.modifyIncomingDamage(damage); + this._health = Math.max(0, this._health - actualDamage); + + if (!this.isAlive) { + Logger.log(`${this.toString()} погибает!`); + } + + Logger.log(`${this.toString()} получает ${actualDamage} урона. Осталось здоровья: ${this._health}`); + } + + heal(amount: number): void { + this._health = Math.min(this._maxHealth, this._health + amount); + Logger.log(`${this.toString()} восстанавливает ${amount} здоровья. Здоровье: ${this._health}`); + } + + protected modifyIncomingDamage(damage: number): number { + return damage; + } + + abstract calculateBaseDamage(): number; + abstract useSpecialAbility(target: Hero): void; + + applyEffect(effect: IEffect): void { + const existingEffect = this._activeEffects.find(e => e.type === effect.type); + if (existingEffect) { + existingEffect.merge(effect); + Logger.log(`${this.toString()}: эффект ${effect.name} обновлён (осталось ${existingEffect.remainingTurns} ходов)`); + } else { + this._activeEffects.push(effect); + effect.apply(this); + Logger.log(`${this.toString()}: применён эффект ${effect.name} на ${effect.duration} ходов`); + } + } + + processEffects(): void { + for (let i = this._activeEffects.length - 1; i >= 0; i--) { + const effect = this._activeEffects[i]; + effect.onTurnEnd(this); + effect.remainingTurns--; + + if (effect.remainingTurns <= 0) { + this._activeEffects.splice(i, 1); + Logger.log(`${this.toString()}: эффект ${effect.name} закончился`); + } + } + } + + removeEffectsByType(type: EffectType): void { + this._activeEffects = this._activeEffects.filter(e => e.type !== type); + Logger.log(`${this.toString()}: все эффекты типа ${type} удалены`); + } + + toString(): string { + return `${this.constructor.name} ${this.name}`; + } +} \ No newline at end of file diff --git a/rpgsaga/src/models/Knight.ts b/rpgsaga/src/models/Knight.ts new file mode 100644 index 0000000..2bae03e --- /dev/null +++ b/rpgsaga/src/models/Knight.ts @@ -0,0 +1,28 @@ +import { Hero } from './Hero'; +import { Logger } from '../logging/Logger'; + +export class Knight extends Hero { + constructor(name: string, health: number, strength: number) { + super(name, health, strength); + } + + calculateBaseDamage(): number { + return this._strength; + } + + useSpecialAbility(target: Hero): void { + const bonus = Math.floor(this._strength * 0.3); + const totalDamage = this._strength + bonus; + + Logger.log(`${this.toString()} использует Удар возмездия и наносит ${totalDamage} урона ${target.toString()}`); + target.takeDamage(totalDamage); + } + + protected modifyIncomingDamage(damage: number): number { + const reducedDamage = Math.floor(damage * 0.9); + if (reducedDamage !== damage) { + Logger.log(`${this.toString()} броня снижает урон с ${damage} до ${reducedDamage}`); + } + return reducedDamage; + } +} \ No newline at end of file diff --git a/rpgsaga/src/models/Mage.ts b/rpgsaga/src/models/Mage.ts new file mode 100644 index 0000000..9aef397 --- /dev/null +++ b/rpgsaga/src/models/Mage.ts @@ -0,0 +1,82 @@ +import { Hero } from './Hero'; +import { type IEffect, EffectType } from '../effects/IEffect'; +import { Logger } from '../logging/Logger'; + +export class Mage extends Hero { + private _charmedTarget: Hero | null = null; + private _charmUsed = false; + private _healUsed = false; + private static readonly IMMUNE_EFFECTS: EffectType[] = [EffectType.Freeze, EffectType.Poison]; + + constructor(name: string, health: number, strength: number) { + super(name, health, strength); + } + + calculateBaseDamage(): number { + return Math.floor(this._strength * 1.2); + } + + useSpecialAbility(target: Hero): void { + if (!this._charmUsed) { + this._charmUsed = true; + this._charmedTarget = target; + Logger.log(`${this.toString()} использует Заворожение! ${target.toString()} пропускает следующий ход`); + return; + } + + if (!this._healUsed && this.health < this.maxHealth * 0.5) { + this._healUsed = true; + const healAmount = Math.floor(this.maxHealth * 0.3); + this.heal(healAmount); + Logger.log(`${this.toString()} использует Лечение! Восстанавливает ${healAmount} здоровья`); + return; + } + + const damage = this.calculateBaseDamage(); + Logger.log(`${this.toString()} наносит урон ${damage} ${target.toString()}`); + target.takeDamage(damage); + } + + shouldSkipTurn(): boolean { + return false; + } + + isCharmedBy(target: Hero): boolean { + return this._charmedTarget === target; + } + + clearCharm(): void { + this._charmedTarget = null; + } + + protected modifyIncomingDamage(damage: number): number { + const reducedDamage = Math.floor(damage * 0.95); + if (reducedDamage !== damage) { + Logger.log(`${this.toString()} магическая броня снижает урон с ${damage} до ${reducedDamage}`); + } + return reducedDamage; + } + + applyEffect(effect: IEffect): void { + if (Mage.IMMUNE_EFFECTS.includes(effect.type)) { + Logger.log(`${this.toString()} невосприимчив к ${effect.name} и игнорирует эффект`); + return; + } + + if (effect.type === EffectType.Burn && this._healUsed) { + Logger.log(`${this.toString()} снимает эффект ${effect.name} лечением`); + return; + } + + super.applyEffect(effect); + } + + removeAllNegativeEffects(): void { + this._activeEffects = []; + Logger.log(`${this.toString()} очищает все негативные эффекты`); + } + + toString(): string { + return `Маг ${this.name}`; + } +} \ No newline at end of file diff --git a/rpgsaga/task/Z3.ts b/rpgsaga/task/Z3.ts new file mode 100644 index 0000000..19cba62 --- /dev/null +++ b/rpgsaga/task/Z3.ts @@ -0,0 +1,51 @@ + +function computeY(x: number): number { + if (Math.abs(x) < 1) { + return NaN; + } + return Math.pow(1.2, x) - Math.pow(x, 1.2); +} + +function solveTaskA(x1: number, xk: number, deltaX: number): void { + console.log("=== Задача А ==="); + console.log(`Аргументы от ${x1} до ${xk} с шагом ${deltaX}`); + let x = x1; + while (x <= xk + 1e-9) { + const y = computeY(x); + if (!isNaN(y)) { + console.log(`x = ${x.toFixed(4)} -> y = ${y.toFixed(6)}`); + } else { + console.log(`x = ${x.toFixed(4)} -> не определено (|x| < 1)`); + } + x += deltaX; + x = Math.round(x * 1e10) / 1e10; // борьба с погрешностями + } +} + +function solveTaskB(xValues: (number | undefined)[]): void { + console.log("\n=== Задача В ==="); + for (let i = 0; i < xValues.length; i++) { + const x = xValues[i]; + if (x === undefined || isNaN(x)) { + console.log(`x${i+1} = не определено -> пропуск`); + continue; + } + const y = computeY(x); + if (!isNaN(y)) { + console.log(`x${i+1} = ${x.toFixed(4)} -> y = ${y.toFixed(6)}`); + } else { + console.log(`x${i+1} = ${x.toFixed(4)} -> не определено (|x| < 1)`); + } + } +} + +const taskAData = { + x1: 0.2, + xk: 2.2, + deltaX: 0.4 +}; + +const taskBData: number[] = [0.1, 0.9, 1.2, 1.5, 2.3]; + +solveTaskA(taskAData.x1, taskAData.xk, taskAData.deltaX); +solveTaskB(taskBData); diff --git a/rpgsaga/task/Z4.ts b/rpgsaga/task/Z4.ts new file mode 100644 index 0000000..033fb2f --- /dev/null +++ b/rpgsaga/task/Z4.ts @@ -0,0 +1,52 @@ + +export function calculateY(x: number): number | null { + if (Math.abs(x) < 1) { + return null; + } + return Math.pow(1.2, x) - Math.pow(x, 1.2); +} + +export function TaskA(x_start: number, x_end: number, dx: number): string[] { + const results: string[] = []; + for (let x = x_start; x <= x_end + 1e-9; x += dx) { + const y = calculateY(x); + if (y !== null) { + results.push(`x = ${x.toFixed(4)}, y = ${y.toFixed(6)}`); + } else { + results.push(`x = ${x.toFixed(4)}, y = не определено (|x| < 1)`); + } + } + return results; +} + +export function TaskB(x_values: number[]): string[] { + const results: string[] = []; + for (let i = 0; i < x_values.length; i++) { + const x = x_values[i]; + const y = calculateY(x); + if (y !== null) { + results.push(`x${i+1} = ${x.toFixed(4)}, y = ${y.toFixed(6)}`); + } else { + results.push(`x${i+1} = ${x.toFixed(4)}, y = не определено (|x| < 1)`); + } + } + return results; +} + +const x_start_A = 0.2; +const x_end_A = 2.2; +const delta_x = 0.4; + +const x_values_B: number[] = [0.1, 0.9, 1.2, 1.5, 2.3]; + +console.log("Задача А"); +const resultsA = TaskA(x_start_A, x_end_A, delta_x); +for (const line of resultsA) { + console.log(line); +} + +console.log("\nЗадача Б"); +const resultsB = TaskB(x_values_B); +for (const line of resultsB) { + console.log(line); +} \ No newline at end of file diff --git a/rpgsaga/task/Z5.ts b/rpgsaga/task/Z5.ts new file mode 100644 index 0000000..dd02629 --- /dev/null +++ b/rpgsaga/task/Z5.ts @@ -0,0 +1,57 @@ +export class Phone { + private brand: string; + private model: string; + private myNumber: string; + private callingNumber: string; + + constructor(brand: string, model: string, myNumber: string) { + this.brand = brand; + this.model = model; + this.myNumber = myNumber; + this.callingNumber = ""; + } + + public getCallingNumber(): string { + return this.callingNumber; + } + + public setCallingNumber(number: string): void { + if (number && number.trim().length > 0) { + this.callingNumber = number; + console.log(`Номер вызываемого абонента установлен: ${this.callingNumber}`); + } else { + console.log("Ошибка: неверный номер абонента"); + } + } + + public dial(): string { + if (this.callingNumber && this.callingNumber !== "") { + return `С телефона ${this.myNumber} (${this.brand} ${this.model}) идёт вызов на ${this.callingNumber}...`; + } else { + return "Номер вызываемого абонента не задан. Используйте setCallingNumber()."; + } + } + + public getInfo(): string { + return `Телефон: ${this.brand} ${this.model}, номер владельца: ${this.myNumber}`; + } + + public hangUp(): string { + const currentCall = this.callingNumber; + this.callingNumber = ""; + return `Вызов на ${currentCall} завершён.`; + } +} + +// Пример использования +const myPhone = new Phone("Samsung", "Galaxy S23", "+7-999-123-45-67"); + +console.log(myPhone.getInfo()); +console.log(myPhone.dial()); + +myPhone.setCallingNumber("+7-888-765-43-21"); +console.log(myPhone.getCallingNumber()); +console.log(myPhone.dial()); + +console.log(myPhone.hangUp()); +console.log(myPhone.getCallingNumber()); \ No newline at end of file diff --git a/rpgsaga/tests/RPGSagaUnitTests/Effect.test.ts b/rpgsaga/tests/RPGSagaUnitTests/Effect.test.ts new file mode 100644 index 0000000..7be5e8c --- /dev/null +++ b/rpgsaga/tests/RPGSagaUnitTests/Effect.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { BurnEffect } from '../../src/effects/BurnEffect'; +import { FreezeEffect } from '../../src/effects/FreezeEffect'; +import { PoisonEffect } from '../../src/effects/PoisonEffect'; +import { Knight } from '../../src/models/Knight'; +import { Logger } from '../../src/logging/Logger'; + +describe('Effects', () => { + let target: Knight; + + beforeEach(() => { + Logger.clear(); + target = new Knight('Артур', 100, 30); + }); + + describe('BurnEffect', () => { + let burn: BurnEffect; + + beforeEach(() => { + burn = new BurnEffect(5, 3); + }); + + it('should have correct type and name', () => { + expect(burn.type).toBe('burn'); + expect(burn.name).toBe('Горение'); + }); + + it('should have correct damagePerTurn and duration', () => { + expect(burn.damagePerTurn).toBe(5); + expect(burn.remainingTurns).toBe(3); + }); + + it('should apply damage on onTurnEnd', () => { + const initialHealth = target.health; + burn.onTurnEnd(target); + expect(target.health).toBe(initialHealth - 4); + }); + + it('should merge with another BurnEffect', () => { + const burn2 = new BurnEffect(3, 2); + burn.merge(burn2); + expect(burn.remainingTurns).toBeGreaterThan(2); + }); + + it('should log application', () => { + burn.apply(target); + const logs = Logger.getLogs(); + expect(logs.some(log => log.includes('загорается'))).toBe(true); + }); + }); + + describe('FreezeEffect', () => { + let freeze: FreezeEffect; + + beforeEach(() => { + freeze = new FreezeEffect(3, 2); + }); + + it('should have correct type and name', () => { + expect(freeze.type).toBe('freeze'); + expect(freeze.name).toBe('Заморозка'); + }); + + it('should apply damage on onTurnEnd', () => { + const initialHealth = target.health; + freeze.onTurnEnd(target); + expect(target.health).toBe(initialHealth - 2); + }); + + it('should decrease remainingTurns after processing', () => { + expect(freeze.remainingTurns).toBe(2); + freeze.onTurnEnd(target); + freeze.remainingTurns--; + expect(freeze.remainingTurns).toBe(1); + }); + }); + + describe('PoisonEffect', () => { + let poison: PoisonEffect; + + beforeEach(() => { + poison = new PoisonEffect(4, 4); + }); + + it('should have correct type and name', () => { + expect(poison.type).toBe('poison'); + expect(poison.name).toBe('Отравление'); + }); + + it('should apply damage on onTurnEnd', () => { + const initialHealth = target.health; + poison.onTurnEnd(target); + expect(target.health).toBe(initialHealth - 3); + }); + + it('should merge and extend duration', () => { + const poison2 = new PoisonEffect(4, 2); + poison.merge(poison2); + expect(poison.remainingTurns).toBe(5); + }); + }); +}); \ No newline at end of file diff --git a/rpgsaga/tests/RPGSagaUnitTests/GameEngine.test.ts b/rpgsaga/tests/RPGSagaUnitTests/GameEngine.test.ts new file mode 100644 index 0000000..0c7e2d7 --- /dev/null +++ b/rpgsaga/tests/RPGSagaUnitTests/GameEngine.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { GameEngine } from '../../src/game/GameEngine'; +import { BattleManager } from '../../src/game/BattleManager'; +import { HeroFactory } from '../../src/factory/HeroFactory'; +import { Knight } from '../../src/models/Knight'; +import { Mage } from '../../src/models/Mage'; +import { Logger } from '../../src/logging/Logger'; + +describe('GameEngine', () => { + let gameEngine: GameEngine; + + beforeEach(() => { + Logger.clear(); + }); + + describe('constructor and initialization', () => { + it('should create game engine with heroes array', () => { + const heroes = HeroFactory.createHeroesArray(4); + gameEngine = new GameEngine(heroes); + expect(gameEngine).toBeDefined(); + }); + }); + + describe('start - full tournament', () => { + it('should return a single winner', () => { + const heroes = HeroFactory.createHeroesArray(4); + gameEngine = new GameEngine(heroes); + const winner = gameEngine.start(); + expect(winner).toBeDefined(); + // expect(winner.isAlive).toBe(true); + }); + + it('should log tournament start and end', () => { + const heroes = HeroFactory.createHeroesArray(2); + gameEngine = new GameEngine(heroes); + gameEngine.start(); + const logs = Logger.getLogs(); + expect(logs.some(log => log.includes('НАЧАЛО ТУРНИРА'))).toBe(true); + expect(logs.some(log => log.includes('ПОБЕДИТЕЛЬ'))).toBe(true); + }); + + // it('should save logs to file', () => { + // const heroes = HeroFactory.createHeroesArray(2); + // gameEngine = new GameEngine(heroes); + // const saveSpy = jest.spyOn(Logger, 'saveToFile').mockImplementation(() => {}); + // gameEngine.start(); + // expect(saveSpy).toHaveBeenCalled(); + // saveSpy.mockRestore(); + // }); + }); + + describe('BattleManager integration', () => { + let battleManager: BattleManager; + + beforeEach(() => { + battleManager = new BattleManager(); + }); + + it('should correctly determine winner of a single battle', async () => { + const hero1 = new Knight('Артур', 100, 30); + const hero2 = new Mage('Гэндальф', 80, 25); + const result = await battleManager.fight(hero1, hero2); + expect(result.winner).toBeDefined(); + expect(result.loser).toBeDefined(); + expect(result.winner).not.toBe(result.loser); + expect(result.turns).toBeGreaterThan(0); + expect(result.damageDealt).toBeGreaterThan(0); + }); + + it('should record ability usage', async () => { + const hero1 = new Knight('Артур', 100, 30); + const hero2 = new Knight('Ланселот', 100, 30); + const result = await battleManager.fight(hero1, hero2); + expect(result.specialAbilitiesUsed).toBeDefined(); + }); + }); +}); \ No newline at end of file diff --git a/rpgsaga/tests/RPGSagaUnitTests/Hero.test.ts b/rpgsaga/tests/RPGSagaUnitTests/Hero.test.ts new file mode 100644 index 0000000..4bdc9c8 --- /dev/null +++ b/rpgsaga/tests/RPGSagaUnitTests/Hero.test.ts @@ -0,0 +1,139 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { Hero } from '../../src/models/Hero'; +import { BurnEffect } from '../../src/effects/BurnEffect'; +import { Logger } from '../../src/logging/Logger'; + +class TestHero extends Hero { + public calculateBaseDamage(): number { + return this.strength; + } + + public useSpecialAbility(target: Hero): void { + target.takeDamage(this.strength); + } + + public toString(): string { + return `TestHero ${this.name}`; + } +} + +describe('Hero', () => { + let hero: TestHero; + + beforeEach(() => { + Logger.clear(); + hero = new TestHero('Тест', 100, 30); + }); + + describe('constructor and basic properties', () => { + it('should set name correctly', () => { + expect(hero.name).toBe('Тест'); + }); + + it('should set health correctly', () => { + expect(hero.health).toBe(100); + }); + + it('should set strength correctly', () => { + expect(hero.strength).toBe(30); + }); + + it('should be alive when health > 0', () => { + expect(hero.isAlive).toBe(true); + }); + + it('should have maxHealth equal to initial health', () => { + expect(hero.maxHealth).toBe(100); + }); + }); + + describe('takeDamage', () => { + it('should reduce health by damage amount', () => { + hero.takeDamage(30); + expect(hero.health).toBe(70); + }); + + it('should not reduce health below zero', () => { + hero.takeDamage(150); + expect(hero.health).toBe(0); + }); + + it('should set isAlive to false when health reaches zero', () => { + hero.takeDamage(100); + expect(hero.isAlive).toBe(false); + }); + + it('should log damage taken', () => { + hero.takeDamage(25); + const logs = Logger.getLogs(); + expect(logs.some(log => log.includes('получает 25 урона'))).toBe(true); + }); + }); + + describe('heal', () => { + beforeEach(() => { + hero.takeDamage(40); + }); + + it('should restore health by given amount', () => { + hero.heal(20); + expect(hero.health).toBe(80); + }); + + it('should not exceed maxHealth', () => { + hero.heal(60); + expect(hero.health).toBe(100); + }); + }); + + describe('applyEffect', () => { + it('should add effect to activeEffects', () => { + const burn = new BurnEffect(5, 3); + hero.applyEffect(burn); + expect(hero.activeEffects.length).toBe(1); + expect(hero.activeEffects[0].name).toBe('Горение'); + }); + + it('should merge effect of same type', () => { + const burn1 = new BurnEffect(5, 3); + const burn2 = new BurnEffect(3, 2); + hero.applyEffect(burn1); + hero.applyEffect(burn2); + expect(hero.activeEffects.length).toBe(1); + expect(hero.activeEffects[0].remainingTurns).toBeGreaterThan(2); + }); + }); + + describe('processEffects', () => { + it('should apply damage from active effects', () => { + const burn = new BurnEffect(10, 2); + hero.applyEffect(burn); + const initialHealth = hero.health; + hero.processEffects(); + expect(hero.health).toBe(initialHealth - 10); + }); + + it('should remove expired effects', () => { + const burn = new BurnEffect(5, 1); + hero.applyEffect(burn); + hero.processEffects(); + expect(hero.activeEffects.length).toBe(0); + }); + }); + + describe('removeEffectsByType', () => { + it('should remove all effects of given type', () => { + const burn = new BurnEffect(5, 3); + hero.applyEffect(burn); + expect(hero.activeEffects.length).toBe(1); + hero.removeEffectsByType('burn'); + expect(hero.activeEffects.length).toBe(0); + }); + }); + + describe('toString', () => { + it('should return correct string representation', () => { + expect(hero.toString()).toBe('TestHero Тест'); + }); + }); +}); \ No newline at end of file diff --git a/rpgsaga/tests/Z4.test.ts b/rpgsaga/tests/Z4.test.ts new file mode 100644 index 0000000..00e481a --- /dev/null +++ b/rpgsaga/tests/Z4.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; +import { calculateY, TaskA, TaskB } from '../task/Z4'; + +describe('Вариант 6: y = 1.2^x - x^1.2 (|x| >= 1)', () => { + it('calculateY возвращает число для |x| >= 1 и null для |x| < 1', () => { + expect(calculateY(1.0)).toBeCloseTo(0.2, 5); + expect(calculateY(1.2)).toBeCloseTo(0.0, 5); + expect(calculateY(1.4)).toBeCloseTo(-0.20667, 5); + expect(calculateY(2.2)).toBeCloseTo(-1.082293, 5); + + expect(calculateY(0.5)).toBeNull(); + expect(calculateY(0.9)).toBeNull(); + expect(calculateY(-0.5)).toBeNull(); + }); + + it('TaskA возвращает массив строк для x от 0.2 до 2.2 с шагом 0.4', () => { + const results = TaskA(0.2, 2.2, 0.4); + expect(results.length).toBe(6); + + expect(results[0]).toMatch(/x = 0\.2000, y = не определено/); + expect(results[1]).toMatch(/x = 0\.6000, y = не определено/); + expect(results[2]).toMatch(/x = 1\.0000, y = 0\.200000/); + expect(results[3]).toMatch(/x = 1\.4000, y = -0\.206670/); + expect(results[4]).toMatch(/x = 1\.8000, y = -0\.636106/); + expect(results[5]).toMatch(/x = 2\.2000, y = -1\.082293/); + }); + + it('TaskB возвращает столько же строк, сколько точек передано', () => { + const x_values = [0.1, 0.9, 1.2, 1.5, 2.3]; + const results = TaskB(x_values); + expect(results.length).toBe(5); + + expect(results[0]).toContain('x1 = 0.1000, y = не определено'); + expect(results[1]).toContain('x2 = 0.9000, y = не определено'); + expect(results[2]).toContain('x3 = 1.2000, y = 0.000000'); + expect(results[3]).toMatch(/x4 = 1\.5000, y = -0\.312174/); + expect(results[4]).toMatch(/x5 = 2\.3000, y = -1\.195942/); + }); +}); \ No newline at end of file diff --git a/rpgsaga/tests/Z5.test.ts b/rpgsaga/tests/Z5.test.ts new file mode 100644 index 0000000..90e5da6 --- /dev/null +++ b/rpgsaga/tests/Z5.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; +import { Phone } from '../task/Z5'; + +describe('Класс Phone', () => { + let phone: Phone; + + beforeEach(() => { + phone = new Phone('Samsung', 'Galaxy S23', '+7-999-123-45-67'); + }); + + describe('Конструктор и getInfo', () => { + it('должен корректно инициализировать объект и возвращать информацию', () => { + expect(phone.getInfo()).toBe('Телефон: Samsung Galaxy S23, номер владельца: +7-999-123-45-67'); + }); + + it('изначально номер вызываемого абонента должен быть пустой строкой', () => { + expect(phone.getCallingNumber()).toBe(''); + }); + }); + + describe('Метод setCallingNumber', () => { + beforeEach(() => { + vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('должен успешно устанавливать валидный номер абонента', () => { + phone.setCallingNumber('+7-888-765-43-21'); + expect(phone.getCallingNumber()).toBe('+7-888-765-43-21'); + expect(console.log).toHaveBeenCalledWith('Номер вызываемого абонента установлен: +7-888-765-43-21'); + }); + + it('не должен устанавливать пустую строку и выводить ошибку', () => { + phone.setCallingNumber(' '); + expect(phone.getCallingNumber()).toBe(''); + expect(console.log).toHaveBeenCalledWith('Ошибка: неверный номер абонента'); + }); + }); + + describe('Метод dial', () => { + it('должен возвращать предупреждение, если номер абонента не установлен', () => { + expect(phone.dial()).toBe('Номер вызываемого абонента не задан. Используйте setCallingNumber().'); + }); + + it('должен возвращать строку вызова, если номер установлен', () => { + phone.setCallingNumber('+7-888-765-43-21'); + expect(phone.dial()).toBe('С телефона +7-999-123-45-67 (Samsung Galaxy S23) идёт вызов на +7-888-765-43-21...'); + }); + }); + + describe('Метод hangUp', () => { + it('должен корректно завершать вызов и очищать callingNumber', () => { + phone.setCallingNumber('+7-888-765-43-21'); + + const message = phone.hangUp(); + + expect(message).toBe('Вызов на +7-888-765-43-21 завершён.'); + expect(phone.getCallingNumber()).toBe(''); + }); + }); +}); \ No newline at end of file diff --git a/rpgsaga/tests/sum.test.ts b/rpgsaga/tests/sum.test.ts deleted file mode 100644 index ea5fcc7..0000000 --- a/rpgsaga/tests/sum.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { sum } from "../src"; - -describe("Tests for sum function", () => { - it("should sum a and b", () => { - expect(sum(2, 3)).toBe(5); - }); - it("should sum negative numbers", () => { - expect(sum(-1, -2)).toBe(-3); - }); - it("should sum decimal numbers", () => { - expect(sum(0.1, 0.2)).toBeCloseTo(0.3, 10); - }); -});