experiments-monogame/UPWG/UPWG.Core/Battle.cs

367 lines
11 KiB
C#
Executable File

using System;
using System.Collections.Generic;
namespace UPWG.Core
{
public enum WrestlerPosition
{
InRing,
OnRopes,
OutsideRing,
}
public enum MomentumLevel
{
None = 0,
Low = 1,
Medium = 2,
High = 3,
}
public enum HitResult
{
Miss,
Hit,
Critical,
}
public class MoveResult
{
public HitResult Result;
// what did this do to the recipient?
public uint RecipientDamage;
public int RecipientMomentumDelta;
public BodyParts RecipientRestrained;
// and what did this do to us?
public uint SelfDamage;
public int SelfMomentumDelta;
public int SelfPopularityDelta;
public MoveResult(
HitResult result,
uint recipientDamage,
int recipientMomentumDelta = 0,
BodyParts restrained = 0,
uint selfDamage = 0,
int selfMomentumDelta = 0,
int selfPopularityDelta = 0
)
{
Result = result;
RecipientDamage = recipientDamage;
RecipientMomentumDelta = recipientMomentumDelta;
RecipientRestrained = restrained;
SelfDamage = selfDamage;
SelfMomentumDelta = selfMomentumDelta;
SelfPopularityDelta = selfPopularityDelta;
}
}
interface IGoal
{
public void ProcessMove(Move move, MoveResult result);
public bool IsComplete();
}
class MaxMomentumGoal : IGoal
{
public int GoalMomentum { get; }
private int _maxMomentum;
private int _curMomentum;
MaxMomentumGoal(int goalMomentum)
{
GoalMomentum = goalMomentum;
}
public void ProcessMove(Move move, MoveResult result)
{
_curMomentum += result.SelfMomentumDelta;
if (_curMomentum > _maxMomentum)
_maxMomentum = _curMomentum;
}
public bool IsComplete()
{
return _maxMomentum >= GoalMomentum;
}
}
public enum MatchStatus
{
InProgress,
Pinfall,
Submission,
CountOut,
Disqualification,
}
public class MoveOptions
{
// this class represents the available moves
public string Strike { get; set; }
public string Grapple { get; set; }
public string Other { get; set; }
public string Special { get; set; }
}
public class Battle
{
private static Random _random = new Random();
public enum ParticipantRole
{
LeftSingle,
RightSingle,
LeftTag1,
LeftTag2,
RightTag1,
RightTag2,
}
public struct Participant
{
public Wrestler Wrestler;
public WrestlerPosition Position;
public BodyParts Restrained;
public BodyParts Injured;
public MomentumLevel Momentum;
public uint MaxFatigue;
public uint CurFatigue;
public Participant(Wrestler wrestler)
{
Wrestler = wrestler;
Position = WrestlerPosition.InRing;
Restrained = 0;
Injured = 0;
Momentum = MomentumLevel.None;
CurFatigue = MaxFatigue = wrestler.Fortitude * 10;
}
}
private uint _turnClock;
private ParticipantRole _playerRole;
public Dictionary<ParticipantRole, Participant> Participants { get; }
public MatchStatus Status { get; private set; }
public ParticipantRole[] UpcomingTurns { get; set; }
public MoveOptions CurrentMoveOptions { get; private set; }
private void InitializeState()
{
_turnClock = 1;
Status = MatchStatus.InProgress;
UpcomingTurns = new ParticipantRole[5];
CalcUpcomingTurns();
CurrentMoveOptions = new MoveOptions();
CalcPlayerMoveOptions();
}
public Battle(Wrestler leftWrestler, Wrestler rightWrestler)
{
Participants = new Dictionary<ParticipantRole, Participant>
{
[ParticipantRole.LeftSingle] = new Participant(leftWrestler),
[ParticipantRole.RightSingle] = new Participant(rightWrestler)
};
_playerRole = ParticipantRole.LeftSingle;
InitializeState();
}
public Battle(Wrestler leftOne, Wrestler leftTwo, Wrestler rightOne, Wrestler rightTwo)
{
Participants = new Dictionary<ParticipantRole, Participant>
{
[ParticipantRole.LeftTag1] = new Participant(leftOne),
[ParticipantRole.LeftTag2] = new Participant(leftTwo),
[ParticipantRole.RightTag1] = new Participant(rightOne),
[ParticipantRole.RightTag2] = new Participant(rightTwo)
};
_playerRole = ParticipantRole.LeftTag1;
InitializeState();
}
// Player Interaction
public void CalcPlayerMoveOptions()
{
// TODO: revisit for other match types
var player = Participants[_playerRole];
var target = Participants[ParticipantRole.RightSingle];
// TODO: revisit for other move types
CurrentMoveOptions.Strike = GetBestAvailableMove(player, MoveType.Strike, target);
CurrentMoveOptions.Grapple = GetBestAvailableMove(player, MoveType.Aerial, target);
CurrentMoveOptions.Special = "Pin";
CurrentMoveOptions.Other = "Enzugiri **";
}
public MoveResult ApplyMove(string moveName)
{
var move = MoveDatabase.Lookup(moveName);
return ApplyMove(_playerRole, move, ParticipantRole.RightSingle);
}
public bool IsPlayerTurn()
{
return UpcomingTurns[0] == _playerRole;
}
public void EndTurn()
{
_turnClock += 1;
}
// CPU Control
public void GetCpuMove(out string move, out ParticipantRole target)
{
move = "Chop 1";
target = ParticipantRole.LeftSingle;
}
// Internals
private MoveResult ApplyMove(ParticipantRole performerPos, Move move, ParticipantRole recipientPos)
{
var performer = Participants[performerPos];
var recipient = Participants[recipientPos];
// can this move be done?
if (!MoveIsValid(performer, move, recipient))
throw new InvalidMoveException();
var hitResult = CheckForHit(performer, move, recipient);
// if move misses, no damage
if (hitResult == HitResult.Miss)
return new MoveResult(hitResult, 0);
uint damage = 0;
uint selfDamage = 0;
switch (move.Type)
{
case MoveType.Strike:
damage = (uint) (move.BaseOffense * performer.Wrestler.DamageMult);
break;
case MoveType.Aerial:
damage = (uint) (move.BaseOffense * performer.Wrestler.DamageMult);
selfDamage = 5;
break;
case MoveType.Hold:
break;
case MoveType.Throw:
damage = (uint) (move.BaseOffense * performer.Wrestler.DamageMult);
break;
case MoveType.Pin:
break;
}
// TODO: how does momentum/popularity work?
return new MoveResult(hitResult, damage, 0, 0, selfDamage, 0, 0);
}
private void CalcUpcomingTurns()
{
for (var i = 0; i < UpcomingTurns.Length; i++)
{
// TODO: use speed to determine turn order
if ((_turnClock + i) % 2 == 1)
{
UpcomingTurns[i] = ParticipantRole.LeftSingle;
}
else
{
UpcomingTurns[i] = ParticipantRole.RightSingle;
}
}
}
private static readonly double[] HitMod = {0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4};
private static readonly double[] EvadeMod = {-0.1, -0.1, -0.05, -0.05, 0, 0.05, 0.05, 0.1, 0.1, 0.15};
private static HitResult CheckForHit(Participant performer, Move move, Participant recipient)
{
var critChance = 0.05;
uint attackStat = 0;
switch (move.Type)
{
case MoveType.Strike:
attackStat = performer.Wrestler.Strength;
break;
case MoveType.Aerial:
attackStat = performer.Wrestler.Agility;
break;
case MoveType.Pin:
attackStat = performer.Wrestler.Strength > performer.Wrestler.Agility
? performer.Wrestler.Strength
: performer.Wrestler.Agility;
break;
case MoveType.Hold:
attackStat = performer.Wrestler.Grappling;
break;
case MoveType.Throw:
attackStat = performer.Wrestler.Grappling;
break;
}
var hitChance = move.BaseChance * HitMod[attackStat] - EvadeMod[recipient.Wrestler.Agility];
var hitRoll = _random.NextDouble();
if (hitRoll < critChance)
return HitResult.Critical;
else if (hitRoll <= hitChance)
return HitResult.Hit;
else
return HitResult.Miss;
}
private static bool MoveIsValid(Participant performer, Move move, Participant recipient)
{
// TODO: check position of performer and recipient
// is there an issue affecting the required body part
var requiredPartsRestrained = performer.Restrained & move.PartsUtilized;
if (requiredPartsRestrained != 0)
return false;
// does wrestler have momentum?
if (performer.Momentum < (MomentumLevel) (move.Rank - 1))
return false;
return true;
}
private static string GetBestAvailableMove(Participant participant, MoveType type, Participant recipient)
{
List<Move> best = new();
var bestRank = -1;
foreach (var (moveName, expertise) in participant.Wrestler.MoveSet)
{
var move = MoveDatabase.Lookup(moveName);
// accumulate list of best valid moves of the type
if (move.Type == type && MoveIsValid(participant, move, recipient))
{
// for now just use rank, eventually this may be dynamic
if (move.Rank > bestRank)
{
bestRank = (int) move.Rank;
best.Clear();
best.Add(move);
}
else if (move.Rank == bestRank)
{
best.Add(move);
}
}
}
return best[_random.Next(best.Count)].Name;
}
}
}