Added TechCounts and all of the StepParity classes

This commit is contained in:
Michael Votaw
2025-02-11 19:39:03 -08:00
committed by teejusb
parent 10a4a972b0
commit 6109105f97
9 changed files with 2524 additions and 2 deletions
+10 -2
View File
@@ -54,14 +54,22 @@ list(APPEND SM_DATA_NOTEDATA_SRC
"NoteDataUtil.cpp"
"NoteDataWithScoring.cpp"
"ColumnCues.cpp"
"MeasureInfo.cpp")
"TechCounts.cpp"
"MeasureInfo.cpp"
"StepParityGenerator.cpp"
"StepParityDatastructs.cpp"
"StepParityCost.cpp")
list(APPEND SM_DATA_NOTEDATA_HPP
"NoteData.h"
"NoteDataUtil.h"
"NoteDataWithScoring.h"
"ColumnCues.h"
"MeasureInfo.h")
"TechCounts.h"
"MeasureInfo.h"
"StepParityGenerator.h"
"StepParityDatastructs.h"
"StepParityCost.h")
source_group("Data Structures\\\\Note Data"
FILES
+891
View File
@@ -0,0 +1,891 @@
#include "global.h"
#include "StepParityCost.h"
#include "NoteData.h"
#include "TechCounts.h"
#include "GameState.h"
using namespace StepParity;
template <typename T>
bool vectorIncludes(const std::vector<T>& vec, const T& value, int columnCount) {
for (int i = 0; i < columnCount; i++)
{
if(vec[i] == value)
{
return true;
}
}
return false;
}
template <typename T>
int indexOf(const std::vector<T>& vec, const T& value, int columnCount) {
for (int i = 0; i < columnCount; i++)
{
if(vec[i] == value)
{
return i;
}
}
return -1;
}
template <typename T>
bool isEmpty(const std::vector<T> & vec, int columnCount) {
for (int i = 0; i < columnCount; i++)
{
if(static_cast<int>(vec[i]) != 0)
{
return false;
}
}
return true;
}
float* StepParityCost::getActionCost(State * initialState, State * resultState, std::vector<Row>& rows, int rowIndex)
{
Row &row = rows[rowIndex];
int columnCount = row.columnCount;
float elapsedTime = resultState->second - initialState->second;
float* costs = new float[NUM_Cost];
for(int i = 0; i < NUM_Cost; i++)
{
costs[i] = 0;
}
std::vector<StepParity::Foot> combinedColumns(columnCount, NONE);
mergeInitialAndResultPosition(initialState, resultState, combinedColumns, columnCount);
// Mine weighting
int leftHeel = -1;
int leftToe = -1;
int rightHeel = -1;
int rightToe = -1;
for (int i = 0; i < columnCount; i++) {
switch (resultState->columns[i]) {
case NONE:
break;
case LEFT_HEEL:
leftHeel = i;
break;
case LEFT_TOE:
leftToe = i;
break;
case RIGHT_HEEL:
rightHeel = i;
break;
case RIGHT_TOE:
rightToe = i;
break;
default:
break;
}
}
costs[COST_MINE] += calcMineCost( initialState, resultState, row, combinedColumns, columnCount);
costs[COST_HOLDSWITCH] += calcHoldSwitchCost( initialState, resultState, row, combinedColumns, columnCount);
costs[COST_BRACKETTAP] += calcBracketTapCost( initialState, resultState, row, leftHeel, leftToe, rightHeel, rightToe, elapsedTime, columnCount);
costs[COST_OTHER] += calcMovingFootWhileOtherIsntOnPadCost( initialState, resultState, columnCount);
bool movedLeft =
resultState->didTheFootMove[LEFT_HEEL] ||
resultState->didTheFootMove[LEFT_TOE];
bool movedRight =
resultState->didTheFootMove[RIGHT_HEEL] ||
resultState->didTheFootMove[RIGHT_TOE];
// Note that this is checking whether the previous state was a jump, not whether the current state is
bool didJump =
((initialState->didTheFootMove[LEFT_HEEL] &&
!initialState->isTheFootHolding[LEFT_HEEL]) ||
(initialState->didTheFootMove[LEFT_TOE] &&
!initialState->isTheFootHolding[LEFT_TOE])) &&
((initialState->didTheFootMove[RIGHT_HEEL] &&
!initialState->isTheFootHolding[RIGHT_HEEL]) ||
(initialState->didTheFootMove[RIGHT_TOE] &&
!initialState->isTheFootHolding[RIGHT_TOE]));
// jacks don't matter if you did a jump before
bool jackedLeft = didJackLeft(initialState, resultState, leftHeel, leftToe, movedLeft, didJump, columnCount);
bool jackedRight = didJackRight(initialState, resultState, rightHeel, rightToe, movedRight, didJump, columnCount);
// Doublestep weighting doesn't apply if you just did a jump or a jack
costs[COST_BRACKETJACK] += calcBracketJackCost( initialState, resultState, rows, rowIndex, movedLeft, movedRight, jackedLeft, jackedRight, didJump, columnCount);
costs[COST_DOUBLESTEP] += calcDoublestepCost(initialState, resultState, rows, rowIndex, movedLeft, movedRight, jackedLeft, jackedRight, didJump, columnCount);
costs[COST_JUMP] += calcJumpCost( row, movedLeft, movedRight, elapsedTime, columnCount);
costs[COST_FACING] += calcFacingCosts( initialState, resultState, combinedColumns, columnCount);
costs[COST_SPIN] += calcSpinCosts(initialState, resultState, combinedColumns, columnCount);
costs[COST_FOOTSWITCH] += caclFootswitchCost( initialState, resultState, row, combinedColumns, elapsedTime, columnCount);
costs[COST_SIDESWITCH] += calcSideswitchCost( initialState, resultState, columnCount);
costs[COST_MISSED_FOOTSWITCH] += calcMissedFootswitchCost( row, jackedLeft, jackedRight, columnCount);
// To do: small weighting for swapping heel with toe or toe with heel (both add up)
// To do: huge weighting for having foot direction opposite of eachother (can't twist one leg 180 degrees)
costs[COST_JACK] += calcJackCost( movedLeft, movedRight, jackedLeft, jackedRight, elapsedTime, columnCount);
// To do: weighting for moving a foot a far distance in a fast time
costs[COST_DISTANCE] += calcBigMovementsQuicklyCost( initialState, resultState, elapsedTime, columnCount);
costs[COST_CROWDED_BRACKET] += calcCrowdedBracketCost(initialState, resultState, elapsedTime, columnCount);
// I don't like that we're updating columns here like this.
// We're basically updating columns with the final position of the feet
// for the next iteration when this is initialState
resultState->columns = combinedColumns;
for(int i = 0; i < columnCount; i++)
{
if(combinedColumns[i] >= NONE)
{
resultState->whereTheFeetAre[combinedColumns[i]] = i;
}
}
for(int i = 0; i < COST_TOTAL; i++)
{
costs[COST_TOTAL] += costs[i];
}
return costs;
}
// This merges the `columns` properties of initialState and resultState, which
// fully represents the player's position on the dance stage.
// For example:
// initialState.columns = [1,0,0,3]
// resultState.columns = [0,1,0,0]
// combinedColumns = [0,1,0,3]
// This eventually gets saved back to resultState
void StepParityCost::mergeInitialAndResultPosition(State * initialState, State * resultState, std::vector<StepParity::Foot> & combinedColumns, int columnCount)
{
// Merge initial + result position
for (int i = 0; i < columnCount; i++) {
// copy in data from resultState over the top which overrides it, as long as it's not nothing
if (resultState->columns[i] != NONE) {
combinedColumns[i] = resultState->columns[i];
continue;
}
// copy in data from initialState, if it wasn't moved
if (
initialState->columns[i] == LEFT_HEEL ||
initialState->columns[i] == RIGHT_HEEL
) {
if (!resultState->didTheFootMove[initialState->columns[i]]) {
combinedColumns[i] = initialState->columns[i];
}
} else if (initialState->columns[i] == LEFT_TOE) {
if (
!resultState->didTheFootMove[LEFT_TOE] &&
!resultState->didTheFootMove[LEFT_HEEL]
) {
combinedColumns[i] = initialState->columns[i];
}
} else if (initialState->columns[i] == RIGHT_TOE) {
if (
!resultState->didTheFootMove[RIGHT_TOE] &&
!resultState->didTheFootMove[RIGHT_HEEL]
) {
combinedColumns[i] = initialState->columns[i];
}
}
}
}
// Calculate the cost of avoiding a mine before the current step
// If a mine occurred just before a step, add to the cost
// ex: 00M0
// 0010 <- add cost
//
// 00M0
// 0100 <- no cost
float StepParityCost::calcMineCost(State * initialState, State * resultState, Row &row, std::vector<StepParity::Foot>& combinedColumns, int columnCount)
{
float cost = 0;
for (int i = 0; i < columnCount; i++) {
if (combinedColumns[i] != NONE && row.mines[i] != 0) {
cost += MINE;
break;
}
}
return cost;
}
// Calculate a cost from having to switch feet in the middle of a hold.
// Multiply the HOLDSWITCH cost by the distance that the "intial" foot
// that was holding the note had to travel to it's new position.
// If the initial foot doesn't move anywhere, then don't mulitply it by anything.
float StepParityCost::calcHoldSwitchCost(State * initialState, State * resultState, Row &row, std::vector<StepParity::Foot> & combinedColumns, int columnCount)
{
float cost = 0;
for (int c = 0; c < columnCount; c++)
{
if (row.holds[c].type == TapNoteType_Empty)
continue;
if (
((combinedColumns[c] == LEFT_HEEL ||
combinedColumns[c] == LEFT_TOE) &&
initialState->columns[c] != LEFT_TOE &&
initialState->columns[c] != LEFT_HEEL) ||
((combinedColumns[c] == RIGHT_HEEL ||
combinedColumns[c] == RIGHT_TOE) &&
initialState->columns[c] != RIGHT_TOE &&
initialState->columns[c] != RIGHT_HEEL)) {
int previousFoot =initialState->whereTheFeetAre[combinedColumns[c]];
cost +=
HOLDSWITCH *
(previousFoot == -1
? 1
: sqrt(
getDistanceSq(layout[c], layout[previousFoot])
));
}
}
return cost;
}
// Calculate the cost of tapping a bracket during a hold note
//
// ex: 0200
// 0000
// 1000 <- maybe bracketable, if left heel is holding Down arrow
// 0300
float StepParityCost::calcBracketTapCost(State * initialState, State * resultState, Row &row, int leftHeel, int leftToe, int rightHeel, int rightToe, float elapsedTime, int columnCount)
{
// Small penalty for trying to jack a bracket during a hold
float cost = 0;
if (leftHeel != -1 && leftToe != -1)
{
float jackPenalty = 1;
if (
initialState->didTheFootMove[LEFT_HEEL] ||
initialState->didTheFootMove[LEFT_TOE])
jackPenalty = 1 / elapsedTime;
if (
row.holds[leftHeel].type != TapNoteType_Empty &&
row.holds[leftToe].type == TapNoteType_Empty) {
cost += BRACKETTAP * jackPenalty;
}
if (
row.holds[leftToe].type != TapNoteType_Empty &&
row.holds[leftHeel].type == TapNoteType_Empty
) {
cost += BRACKETTAP * jackPenalty;
}
}
if (rightHeel != -1 && rightToe != -1) {
float jackPenalty = 1;
if (
initialState->didTheFootMove[RIGHT_TOE] ||
initialState->didTheFootMove[RIGHT_HEEL]
)
jackPenalty = 1 / elapsedTime;
if (
row.holds[rightHeel].type != TapNoteType_Empty &&
row.holds[rightToe].type == TapNoteType_Empty
) {
cost += BRACKETTAP * jackPenalty;
}
if (
row.holds[rightToe].type != TapNoteType_Empty &&
row.holds[rightHeel].type == TapNoteType_Empty
) {
cost += BRACKETTAP * jackPenalty;
}
}
return cost;
}
// Calculate a cost for moving the same foot while the other
// isn't on the pad.
//
float StepParityCost::calcMovingFootWhileOtherIsntOnPadCost(State * initialState, State * resultState, int columnCount)
{
float cost = 0;
// Weighting for moving a foot while the other isn't on the pad (so marked doublesteps are less bad than this)
if (std::any_of(initialState->columns.begin(), initialState->columns.end(), [](Foot elem) { return elem != NONE; }))
{
for (auto f : resultState->movedFeet)
{
switch (f)
{
case LEFT_HEEL:
case LEFT_TOE:
if (
!(
initialState->whereTheFeetAre[RIGHT_HEEL] != -1 ||
initialState->whereTheFeetAre[RIGHT_TOE] != -1))
cost += OTHER;
break;
case RIGHT_HEEL:
case RIGHT_TOE:
if (
!(
initialState->whereTheFeetAre[LEFT_HEEL] != -1 ||
initialState->whereTheFeetAre[LEFT_TOE] != -1))
cost += OTHER;
break;
default:
break;
}
}
}
return cost;
}
float StepParityCost::calcBracketJackCost(State * initialState, State * resultState, std::vector<Row> & rows, int rowIndex, bool movedLeft, bool movedRight, bool jackedLeft, bool jackedRight, bool didJump, int columnCount)
{
float cost = 0;
if (
movedLeft != movedRight &&
(movedLeft || movedRight) &&
isEmpty(resultState->holdFeet, columnCount) &&
!didJump)
{
if (
jackedLeft &&
resultState->didTheFootMove[LEFT_HEEL] &&
resultState->didTheFootMove[LEFT_TOE]
) {
cost += BRACKETJACK;
}
if (
jackedRight &&
resultState->didTheFootMove[RIGHT_HEEL] &&
resultState->didTheFootMove[RIGHT_TOE]
) {
cost += BRACKETJACK;
}
}
return cost;
}
float StepParityCost::calcDoublestepCost(State * initialState, State * resultState, std::vector<Row> & rows, int rowIndex, bool movedLeft, bool movedRight, bool jackedLeft, bool jackedRight, bool didJump, int columnCount)
{
float cost = 0;
if (
movedLeft != movedRight &&
(movedLeft || movedRight) &&
isEmpty(resultState->holdFeet, columnCount) &&
!didJump)
{
bool doublestepped = didDoubleStep(initialState, resultState, rows, rowIndex, movedLeft, jackedLeft, movedRight, jackedRight, columnCount);
if (doublestepped) {
cost += DOUBLESTEP;
}
}
return cost;
}
float StepParityCost::calcJumpCost(Row & row, bool movedLeft, bool movedRight, float elapsedTime, int columnCount)
{
float cost = 0;
if (
movedLeft &&
movedRight &&
std::count_if(row.notes.begin(), row.notes.end(), [](StepParity::IntermediateNoteData note)
{ return note.type != TapNoteType_Empty; }) >= 2)
{
cost += JUMP / elapsedTime;
}
return cost;
}
float StepParityCost::calcMissedFootswitchCost(Row & row, bool jackedLeft, bool jackedRight, int columnCount)
{
float cost = 0;
if (
(jackedLeft || jackedRight) &&
(std::any_of(row.mines.begin(), row.mines.end(), [](int mine)
{ return mine != 0; }) ||
std::any_of(row.fakeMines.begin(), row.fakeMines.end(), [](int mine)
{ return mine != 0; })))
{
cost += MISSED_FOOTSWITCH;
}
return cost;
}
float StepParityCost::calcFacingCosts(State * initialState, State * resultState, std::vector<StepParity::Foot> & combinedColumns, int columnCount)
{
float cost = 0;
float endLeftHeel = -1;
float endLeftToe = -1;
float endRightHeel = -1;
float endRightToe = -1;
for (int i = 0; i < columnCount; i++) {
switch (combinedColumns[i]) {
case NONE:
break;
case LEFT_HEEL:
endLeftHeel = i;
break;
case LEFT_TOE:
endLeftToe = i;
break;
case RIGHT_HEEL:
endRightHeel = i;
break;
case RIGHT_TOE:
endRightToe = i;
default:
break;
}
}
if (endLeftToe == -1) endLeftToe = endLeftHeel;
if (endRightToe == -1) endRightToe = endRightHeel;
// facing backwards gives a bit of bad weight (scaled heavily the further back you angle, so crossovers aren't Too bad; less bad than doublesteps)
float heelFacing =
endLeftHeel != -1 && endRightHeel != -1
? getXDifference(endLeftHeel, endRightHeel)
: 0;
float toeFacing =
endLeftToe != -1 && endRightToe != -1
? getXDifference(endLeftToe, endRightToe)
: 0;
float leftFacing =
endLeftHeel != -1 && endLeftToe != -1
? getYDifference(endLeftHeel, endLeftToe)
: 0;
float rightFacing =
endRightHeel != -1 && endRightToe != -1
? getYDifference(endRightHeel, endRightToe)
: 0;
float heelFacingPenalty = pow(-1 * std::min(heelFacing, 0.0f), 1.8) * 100;
float toesFacingPenalty = pow(-1 * std::min(toeFacing, 0.0f), 1.8) * 100;
float leftFacingPenalty = pow(-1 * std::min(leftFacing, 0.0f), 1.8) * 100;
float rightFacingPenalty = pow(-1 * std::min(rightFacing, 0.0f), 1.8) * 100;
if (heelFacingPenalty > 0)
cost += heelFacingPenalty * FACING;
if (toesFacingPenalty > 0)
cost += toesFacingPenalty * FACING;
if (leftFacingPenalty > 0)
cost += leftFacingPenalty * FACING;
if (rightFacingPenalty > 0)
cost += rightFacingPenalty * FACING;
return cost;
}
float StepParityCost::calcSpinCosts(State * initialState, State * resultState, std::vector<StepParity::Foot> & combinedColumns, int columnCount)
{
float cost = 0;
float endLeftHeel = -1;
float endLeftToe = -1;
float endRightHeel = -1;
float endRightToe = -1;
for (int i = 0; i < columnCount; i++) {
switch (combinedColumns[i]) {
case NONE:
break;
case LEFT_HEEL:
endLeftHeel = i;
break;
case LEFT_TOE:
endLeftToe = i;
break;
case RIGHT_HEEL:
endRightHeel = i;
break;
case RIGHT_TOE:
endRightToe = i;
default:
break;
}
}
if (endLeftToe == -1) endLeftToe = endLeftHeel;
if (endRightToe == -1) endRightToe = endRightHeel;
// spin
StagePoint previousLeftPos = averagePoint(
initialState->whereTheFeetAre[LEFT_HEEL],
initialState->whereTheFeetAre[LEFT_TOE]
);
StagePoint previousRightPos = averagePoint(
initialState->whereTheFeetAre[RIGHT_HEEL],
initialState->whereTheFeetAre[RIGHT_TOE]
);
StagePoint leftPos = averagePoint(endLeftHeel, endLeftToe);
StagePoint rightPos = averagePoint(endRightHeel, endRightToe);
if (
rightPos.x < leftPos.x &&
previousRightPos.x < previousLeftPos.x &&
rightPos.y < leftPos.y &&
previousRightPos.y > previousLeftPos.y
) {
cost += SPIN;
}
if (
rightPos.x < leftPos.x &&
previousRightPos.x < previousLeftPos.x &&
rightPos.y > leftPos.y &&
previousRightPos.y < previousLeftPos.y
) {
cost += SPIN;
}
return cost;
}
// Footswitches are harder the slower they are.
// Add a penalty when they get slower than 8ths at 120bpm (0.25 seconds)
float StepParityCost::caclFootswitchCost(State * initialState, State * resultState, Row & row, std::vector<StepParity::Foot> & combinedColumns, float elapsedTime, int columnCount)
{
float cost = 0;
// ignore footswitch with 24 or less distance (8th note); penalise slower footswitches based on distance
if (elapsedTime >= 0.25) {
// footswitching has no penalty if there's a mine nearby
if (
std::all_of(row.mines.begin(), row.mines.end(), [](int mine)
{ return mine == 0; }) &&
std::all_of(row.fakeMines.begin(), row.fakeMines.end(), [](int mine)
{ return mine == 0; }))
{
float timeScaled = elapsedTime - 0.25;
for (int i = 0; i < columnCount; i++)
{
if (
initialState->columns[i] == NONE ||
resultState->columns[i] == NONE)
continue;
if (
initialState->columns[i] != resultState->columns[i] &&
!resultState->didTheFootMove[initialState->columns[i]])
{
cost += pow(timeScaled / 2.0f, 2) * FOOTSWITCH;
break;
}
}
}
}
return cost;
}
// TODO: This doesn't work for doubles, since it's only checking P1 left and P1 right
float StepParityCost::calcSideswitchCost(State * initialState, State * resultState, int columnCount)
{
float cost = 0;
if (
initialState->columns[0] != resultState->columns[0] &&
resultState->columns[0] != NONE &&
initialState->columns[0] != NONE &&
!resultState->didTheFootMove[initialState->columns[0]])
{
cost += SIDESWITCH;
}
if (
initialState->columns[3] != resultState->columns[3] &&
resultState->columns[3] != NONE &&
initialState->columns[3] != NONE &&
!resultState->didTheFootMove[initialState->columns[3]]
) {
cost += SIDESWITCH;
}
return cost;
}
// Jacks are harder to do the faster they are.
// Add a penalty when they get faster than 16ths at 150bpm (0.1 seconds)
float StepParityCost::calcJackCost(bool movedLeft, bool movedRight, bool jackedLeft, bool jackedRight, float elapsedTime, int columnCount)
{
float cost = 0;
// weighting for jacking two notes too close to eachother
if (elapsedTime < JACK_THRESHOLD && movedLeft != movedRight) {
float timeScaled = JACK_THRESHOLD - elapsedTime;
if (jackedLeft || jackedRight) {
cost += (1 / timeScaled - 1 / JACK_THRESHOLD) * JACK;
}
}
return cost;
}
float StepParityCost::calcBigMovementsQuicklyCost(State * initialState, State * resultState, float elapsedTime, int columnCount)
{
float cost = 0;
for (StepParity::Foot foot : resultState->movedFeet)
{
if(foot == NONE)
continue;
int idxFoot = initialState->whereTheFeetAre[foot];
if (idxFoot == -1)
continue;
cost +=
(sqrt(
getDistanceSq(
layout[idxFoot],
layout[resultState->whereTheFeetAre[foot]])) *
DISTANCE) /
elapsedTime;
}
return cost;
}
// Are we trying to bracket a column that the other foot was just on,
// or are we trying to hit a note that the other foot was just bracketing?
float StepParityCost::calcCrowdedBracketCost(State * initialState, State * resultState, float elapsedTime, int columnCount)
{
float cost = 0;
bool resultLeftBracket = resultState->whereTheFeetAre[LEFT_HEEL] > -1 && resultState->whereTheFeetAre[LEFT_TOE] > -1;
bool resultRightBracket = resultState->whereTheFeetAre[RIGHT_HEEL] > -1 && resultState->whereTheFeetAre[RIGHT_TOE] > -1;
bool initialLeftBracket = initialState->whereTheFeetAre[LEFT_HEEL] > -1 && initialState->whereTheFeetAre[LEFT_TOE] > -1;
bool initialRightBracket = initialState->whereTheFeetAre[RIGHT_HEEL] > -1 && initialState->whereTheFeetAre[RIGHT_TOE] > -1;
// if we're trying to bracket with left foot, does it overlap the right foot
// in previous state?
if(
(resultLeftBracket)
&& (
initialState->columns[resultState->whereTheFeetAre[LEFT_HEEL]] == RIGHT_HEEL ||
initialState->columns[resultState->whereTheFeetAre[LEFT_HEEL]] == RIGHT_TOE ||
initialState->columns[resultState->whereTheFeetAre[LEFT_TOE]] == RIGHT_HEEL ||
initialState->columns[resultState->whereTheFeetAre[LEFT_TOE]] == RIGHT_TOE
)
)
{
cost += CROWDED_BRACKET / elapsedTime;
}
else if(initialLeftBracket
&& (
resultState->columns[initialState->whereTheFeetAre[LEFT_HEEL]] == RIGHT_HEEL ||
resultState->columns[initialState->whereTheFeetAre[LEFT_HEEL]] == RIGHT_TOE ||
resultState->columns[initialState->whereTheFeetAre[LEFT_TOE]] == RIGHT_HEEL ||
resultState->columns[initialState->whereTheFeetAre[LEFT_TOE]] == RIGHT_TOE
)
)
{
cost += CROWDED_BRACKET / elapsedTime;
}
// and if we're trying to bracket with right foot, does it overlap the left ?
if((resultRightBracket )
&& (
initialState->columns[resultState->whereTheFeetAre[RIGHT_HEEL]] == LEFT_HEEL ||
initialState->columns[resultState->whereTheFeetAre[RIGHT_HEEL]] == LEFT_TOE ||
initialState->columns[resultState->whereTheFeetAre[RIGHT_TOE]] == LEFT_HEEL ||
initialState->columns[resultState->whereTheFeetAre[RIGHT_TOE]] == LEFT_TOE
)
)
{
cost += CROWDED_BRACKET / elapsedTime;
}
else if( initialRightBracket
&& (
resultState->columns[initialState->whereTheFeetAre[RIGHT_HEEL]] == LEFT_HEEL ||
resultState->columns[initialState->whereTheFeetAre[RIGHT_HEEL]] == LEFT_TOE ||
resultState->columns[initialState->whereTheFeetAre[RIGHT_TOE]] == LEFT_HEEL ||
resultState->columns[initialState->whereTheFeetAre[RIGHT_TOE]] == LEFT_TOE
)
)
{
cost += CROWDED_BRACKET / elapsedTime;
}
return cost;
}
bool StepParityCost::didDoubleStep(State * initialState, State * resultState, std::vector<Row> & rows, int rowIndex, bool movedLeft, bool jackedLeft, bool movedRight, bool jackedRight, int columnCount)
{
Row &row = rows[rowIndex];
bool doublestepped = false;
if (
movedLeft &&
!jackedLeft &&
((initialState->didTheFootMove[LEFT_HEEL] &&
!initialState->isTheFootHolding[LEFT_HEEL]) ||
(initialState->didTheFootMove[LEFT_TOE] &&
!initialState->isTheFootHolding[LEFT_TOE])))
{
doublestepped = true;
}
if (
movedRight &&
!jackedRight &&
((initialState->didTheFootMove[RIGHT_HEEL] &&
!initialState->isTheFootHolding[RIGHT_HEEL]) ||
(initialState->didTheFootMove[RIGHT_TOE] &&
!initialState->isTheFootHolding[RIGHT_TOE]))
)
doublestepped = true;
if (rowIndex - 1 > -1)
{
StepParity::Row &lastRow = rows[rowIndex - 1];
for (StepParity::IntermediateNoteData hold: lastRow.holds) {
if (hold.type == TapNoteType_Empty) continue;
float endBeat = row.beat;
float startBeat = lastRow.beat;
// if a hold tail extends past the last row & ends in between, we can doublestep
if (
hold.beat + hold.hold_length > startBeat &&
hold.beat + hold.hold_length < endBeat
)
doublestepped = false;
// if the hold tail extends past this row, we can doublestep
if (hold.beat + hold.hold_length >= endBeat) doublestepped = false;
}
}
return doublestepped;
}
bool StepParityCost::didJackLeft(State * initialState, State * resultState, int leftHeel, int leftToe, bool movedLeft, bool didJump, int columnCount)
{
bool jackedLeft = false;
if(!didJump && movedLeft)
{
if ( leftHeel > -1 &&
initialState->columns[leftHeel] == LEFT_HEEL &&
!resultState->isTheFootHolding[LEFT_HEEL] &&
((initialState->didTheFootMove[LEFT_HEEL] &&
!initialState->isTheFootHolding[LEFT_HEEL]) ||
(initialState->didTheFootMove[LEFT_TOE] &&
!initialState->isTheFootHolding[LEFT_TOE]))
) {
jackedLeft = true;
}
if (
leftToe > -1 &&
initialState->columns[leftToe] == LEFT_TOE &&
!resultState->isTheFootHolding[LEFT_TOE] &&
((initialState->didTheFootMove[LEFT_HEEL] &&
!initialState->isTheFootHolding[LEFT_HEEL]) ||
(initialState->didTheFootMove[LEFT_TOE] &&
!initialState->isTheFootHolding[LEFT_TOE]))
){
jackedLeft = true;
}
}
return jackedLeft;
}
bool StepParityCost::didJackRight(State * initialState, State * resultState, int rightHeel, int rightToe, bool movedRight, bool didJump, int columnCount)
{
bool jackedRight = false;
if(!didJump && movedRight)
{
if ( rightHeel > -1 &&
initialState->columns[rightHeel] == RIGHT_HEEL &&
!resultState->isTheFootHolding[RIGHT_HEEL] &&
((initialState->didTheFootMove[RIGHT_HEEL] &&
!initialState->isTheFootHolding[RIGHT_HEEL]) ||
(initialState->didTheFootMove[RIGHT_TOE] &&
!initialState->isTheFootHolding[RIGHT_TOE]))
) {
jackedRight = true;
}
if ( rightToe > -1 &&
initialState->columns[rightToe] == RIGHT_TOE &&
!resultState->isTheFootHolding[RIGHT_TOE] &&
((initialState->didTheFootMove[RIGHT_HEEL] &&
!initialState->isTheFootHolding[RIGHT_HEEL]) ||
(initialState->didTheFootMove[RIGHT_TOE] &&
!initialState->isTheFootHolding[RIGHT_TOE]))
) {
jackedRight = true;
}
}
return jackedRight;
}
float StepParityCost::getDistanceSq(StepParity::StagePoint p1, StepParity::StagePoint p2)
{
return (p1.y - p2.y) * (p1.y - p2.y) + (p1.x - p2.x) * (p1.x - p2.x);
}
float StepParityCost::getPlayerAngle(StepParity::StagePoint left, StepParity::StagePoint right)
{
float x1 = right.x - left.x;
float y1 = right.y - left.y;
float x2 = 1;
float y2 = 0;
float dot = x1 * x2 + y1 * y2;
float det = x1 * y2 - y1 * x2;
return atan2f(det, dot);
}
float StepParityCost::getXDifference(int leftIndex, int rightIndex) {
if (leftIndex == rightIndex) return 0;
float dx = layout[rightIndex].x - layout[leftIndex].x;
float dy = layout[rightIndex].y - layout[leftIndex].y;
float distance = sqrt(dx * dx + dy * dy);
dx /= distance;
bool negative = dx <= 0;
dx = pow(dx, 4);
if (negative) dx = -dx;
return dx;
}
float StepParityCost::getYDifference(int leftIndex, int rightIndex) {
if (leftIndex == rightIndex) return 0;
float dx = layout[rightIndex].x - layout[leftIndex].x;
float dy = layout[rightIndex].y - layout[leftIndex].y;
float distance = sqrt(dx * dx + dy * dy);
dy /= distance;
bool negative = dy <= 0;
dy = pow(dy, 4);
if (negative) dy = -dy;
return dy;
}
StagePoint StepParityCost::averagePoint(int leftIndex, int rightIndex) {
if (leftIndex == -1 && rightIndex == -1) return { 0,0 };
if (leftIndex == -1) return layout[rightIndex];
if (rightIndex == -1) return layout[leftIndex];
return {
(layout[leftIndex].x + layout[rightIndex].x) / 2.0f,
(layout[leftIndex].y + layout[rightIndex].y) / 2.0f,
};
}
+81
View File
@@ -0,0 +1,81 @@
#ifndef STEP_PARITY_COST_H
#define STEP_PARITY_COST_H
#include "GameConstantsAndTypes.h"
#include "StepParityDatastructs.h"
namespace StepParity
{
const int DOUBLESTEP= 850;
const int BRACKETJACK= 20;
const int JACK= 30;
const int JUMP= 30;
const int BRACKETTAP= 400;
const int HOLDSWITCH= 55;
const int MINE= 10000;
const int FOOTSWITCH= 5000;
const int MISSED_FOOTSWITCH= 500;
const int FACING= 2;
const int DISTANCE= 6;
const int SPIN= 1000;
const int SIDESWITCH= 130;
const int CROWDED_BRACKET = 40;
const int OTHER = 500;
const float JACK_THRESHOLD = 0.1;
class StepParityCost
{
private:
StageLayout layout;
public:
StepParityCost(StageLayout _layout)
{
layout = _layout;
}
/// @brief Computes and returns a cost value for the player moving from initialState to resultState.
/// @param initialState The starting position of the player
/// @param resultState The end position of the player
/// @param rows
/// @param rowIndex The index of the row represented by resultState
/// @return The computed cost
float* getActionCost(State * initialState, State * resultState, std::vector<Row> &rows, int rowIndex);
private:
void mergeInitialAndResultPosition(State * initialState, State * resultState, std::vector<StepParity::Foot> &combinedColumns, int columnCount);
float calcMineCost(State * initialState, State * resultState, Row &row, std::vector<StepParity::Foot> &combinedColumns, int columnCount);
float calcHoldSwitchCost(State * initialState, State * resultState, Row &row, std::vector<StepParity::Foot> &combinedColumns, int columnCount);
float calcBracketTapCost(State * initialState, State * resultState, Row &row, int leftHeel, int leftToe, int rightHeel, int rightToe, float elapsedTime, int columnCount);
float calcMovingFootWhileOtherIsntOnPadCost(State * initialState, State * resultState, int columnCount);
float calcBracketJackCost(State * initialState, State * resultState, std::vector<Row> &rows, int rowIndex, bool movedLeft, bool movedRight, bool jackedLeft, bool jackedRight, bool didJump, int columnCount);
float calcDoublestepCost(State * initialState, State * resultState, std::vector<Row> & rows, int rowIndex, bool movedLeft, bool movedRight, bool jackedLeft, bool jackedRight, bool didJump, int columnCount);
float calcJumpCost(Row &row, bool movedLeft, bool movedRight, float elapsedTime, int columnCount);
float calcMissedFootswitchCost(Row &row, bool jackedLeft, bool jackedRight, int columnCount);
float calcFacingCosts(State * initialState, State * resultState, std::vector<StepParity::Foot> &combinedColumns, int columnCount);
float calcSpinCosts(State * initialState, State * resultState, std::vector<StepParity::Foot> & combinedColumns, int columnCount);
float caclFootswitchCost(State * initialState, State * resultState, Row &row, std::vector<StepParity::Foot> &combinedColumns, float elapsedTime, int columnCount);
float calcSideswitchCost(State * initialState, State * resultState, int columnCount);
float calcJackCost(bool movedLeft, bool movedRight, bool jackedLeft, bool jackedRight, float elapsedTime, int columnCount);
float calcBigMovementsQuicklyCost(State * initialState, State * resultState, float elapsedTime, int columnCount);
float calcCrowdedBracketCost(State * initialState, State * resultState, float elapsedTime, int columnCount);
bool didDoubleStep(State * initialState, State * resultState, std::vector<Row> &rows, int rowIndex, bool movedLeft, bool jackedLeft, bool movedRight, bool jackedRight, int columnCount);
bool didJackLeft(State * initialState, State * resultState, int leftHeel, int leftToe, bool movedLeft, bool didJump, int columnCount);
bool didJackRight(State * initialState, State * resultState, int rightHeel, int rightToe, bool movedRight, bool didJump,int columnCount);
float getDistanceSq(StepParity::StagePoint p1, StepParity::StagePoint p2);
float getPlayerAngle(StepParity::StagePoint left, StepParity::StagePoint right);
float getXDifference(int leftIndex, int rightIndex);
float getYDifference(int leftIndex, int rightIndex);
StagePoint averagePoint(int leftIndex, int rightIndex);
};
};
#endif
+250
View File
@@ -0,0 +1,250 @@
#include "global.h"
#include "StepParityDatastructs.h"
using namespace StepParity;
//
// Graph/Node methods
//
int calculateVectorHash(const std::vector<Foot> &vec)
{
int value = 0;
for(Foot f : vec)
{
value *= 5;
value += f;
}
return value;
}
bool State::operator==(const State &other) const
{
return rowIndex == other.rowIndex &&
columns == other.columns &&
movedFeet == other.movedFeet &&
holdFeet == other.holdFeet;
}
bool State::operator<(const State &other) const
{
if(rowIndex != other.rowIndex) {
return rowIndex < other.rowIndex;
}
if(columnsHash != other.columnsHash) {
return columnsHash < other.columnsHash;
}
if(movedFeetHash != other.movedFeetHash) {
return movedFeetHash < other.movedFeetHash;
}
if(holdFeetHash != other.holdFeetHash) {
return holdFeetHash < other.holdFeetHash;
}
return false;
}
void State::calculateHashes()
{
columnsHash = calculateVectorHash(columns);
movedFeetHash = calculateVectorHash(movedFeet);
holdFeetHash = calculateVectorHash(holdFeet);
}
StepParityNode * StepParityGraph::addOrGetExistingNode(const State &state)
{
// this is silly, but the start node has a rowIndex of -1
// which doesn't work as an array index.
int rowIndex = state.rowIndex + 1;
while (static_cast<int>(stateNodeMap.size()) <= rowIndex)
{
stateNodeMap.push_back(std::map<State, StepParityNode *, StateComparator>());
}
if(stateNodeMap[rowIndex].find(state) == stateNodeMap[rowIndex].end())
{
StepParityNode* newNode = new StepParityNode(state);
newNode->id = int(nodes.size());
nodes.push_back(newNode);
newNode->state.idx = int(states.size());
states.push_back(&(newNode->state));
stateNodeMap[rowIndex][state] = newNode;
}
return stateNodeMap[rowIndex][state];
}
//
// Json methods
//
template<typename Container>
Json::Value FeetToJson(const Container& feets, bool useStrings)
{
Json::Value root;
for(Foot f: feets)
{
if(useStrings)
{
root.append(FEET_LABELS[static_cast<int>(f)]);
}
else
{
root.append(static_cast<int>(f));
}
}
return root;
}
Json::Value State::ToJson(bool useStrings)
{
Json::Value root;
Json::Value jsonColumns = FeetToJson(columns, useStrings);
Json::Value jsonMovedFeet = FeetToJson(movedFeet, useStrings);
Json::Value jsonHoldFeet = FeetToJson(holdFeet, useStrings);
root["idx"] = idx;
root["columns"] = jsonColumns;
root["movedFeet"] = jsonMovedFeet;
root["holdFeet"] = jsonHoldFeet;
root["second"] = second;
root["rowIndex"] = rowIndex;
return root;
}
Json::Value IntermediateNoteData::ToJson(bool useStrings)
{
Json::Value root;
if(useStrings)
{
root["type"] = TapNoteTypeShortNames[static_cast<int>(type)];
root["subtype"] = TapNoteSubTypeShortNames[static_cast<int>(subtype)];
root["parity"] = FEET_LABELS[static_cast<int>(parity)];
}
else
{
root["type"] = static_cast<int>(type);
root["subtype"] = static_cast<int>(subtype);
root["parity"] = static_cast<int>(parity);
}
root["col"] = col;
root["row"] = row;
root["beat"] = beat;
root["hold"] = hold_length;
root["warped"] = warped;
root["fake"] = fake;
root["second"] = second;
return root;
}
Json::Value Row::ToJson(bool useStrings)
{
Json::Value root;
Json::Value jsonNotes;
Json::Value jsonHolds;
Json::Value jsonHoldTails;
Json::Value jsonMines;
Json::Value jsonFakeMines;
for (IntermediateNoteData n : notes) { jsonNotes.append(n.ToJson(useStrings)); }
for (IntermediateNoteData n : holds) { jsonHolds.append(n.ToJson(useStrings)); }
for (int t : holdTails) { jsonHoldTails.append(t); }
for (int m : mines) { jsonMines.append(m); }
for (int f : fakeMines) { jsonFakeMines.append(f); }
root["notes"] = jsonNotes;
root["holds"] = jsonHolds;
root["hold_tails"] = jsonHoldTails;
root["mines"] = jsonMines;
root["fake_mines"] = jsonFakeMines;
root["second"] = second;
root["beat"] = beat;
return root;
}
Json::Value StepParityNode::ToJson()
{
Json::Value root;
Json::Value jsonNeighbors;
for (auto it = neighbors.begin(); it != neighbors.end(); it++)
{
Json::Value n;
n["id"] = it->first->id;
Json::Value jsonCosts;
float * costs = it->second;
for(int i = 0; i < NUM_Cost; i++)
{
jsonCosts[COST_LABELS[i]] = costs[i];
}
n["costs"] = jsonCosts;
jsonNeighbors.append(n);
}
root["id"] = id;
root["stateIdx"] = state.idx;
root["neighbors"] = jsonNeighbors;
return root;
}
Json::Value Row::ToJsonRows(const std::vector<Row> & rows, bool useStrings)
{
Json::Value root;
for(Row row: rows)
{
root.append(row.ToJson(useStrings));
}
return root;
}
Json::Value Row::ParityRowsJson(const std::vector<Row> & rows)
{
Json::Value root;
for(Row row: rows)
{
Json::Value pr;
for(IntermediateNoteData note: row.notes)
{
pr.append(static_cast<int>(note.parity));
}
root.append(pr);
}
return root;
}
Json::Value StepParityGraph::ToJson()
{
Json::Value jsonNodes;
for(auto node: nodes)
{
jsonNodes.append(node->ToJson());
}
Json::Value jsonStates;
for(auto state: states)
{
jsonStates.append(state->ToJson(false));
}
Json::Value root;
root["nodes"] = jsonNodes;
root["states"] = jsonStates;
return root;
}
Json::Value StepParityGraph::NodeStateJson()
{
Json::Value root;
for(auto node: nodes)
{
Json::Value nodeJson;
nodeJson["id"] = node->id;
nodeJson["state"] = node->state.ToJson(false);
root.append(nodeJson);
}
return root;
}
+307
View File
@@ -0,0 +1,307 @@
#ifndef STEP_PARITY_DATASTRUCTS_H
#define STEP_PARITY_DATASTRUCTS_H
#include "GameConstantsAndTypes.h"
#include "NoteData.h"
#include "json/json.h"
#include "JsonUtil.h"
#include <queue>
#include <unordered_map>
namespace StepParity {
const float CLM_SECOND_INVALID = -1;
enum Foot
{
NONE = 0,
LEFT_HEEL,
LEFT_TOE,
RIGHT_HEEL,
RIGHT_TOE,
NUM_Foot
};
const std::vector<StepParity::Foot> FEET = {LEFT_HEEL, LEFT_TOE, RIGHT_HEEL, RIGHT_TOE};
const RString FEET_LABELS[] = {"N", "L", "l", "R", "r", "5??", "6??"};
const RString TapNoteTypeShortNames[] = { "Empty", "Tap", "Mine", "Attack", "AutoKeySound", "Fake", "", "" };
const RString TapNoteSubTypeShortNames[] = { "Hold", "Roll", "", "" };
enum Cost
{
COST_DOUBLESTEP = 0,
COST_BRACKETJACK,
COST_JACK,
COST_JUMP,
COST_BRACKETTAP,
COST_HOLDSWITCH,
COST_MINE,
COST_FOOTSWITCH,
COST_MISSED_FOOTSWITCH,
COST_FACING,
COST_DISTANCE,
COST_SPIN,
COST_SIDESWITCH,
COST_CROWDED_BRACKET ,
COST_OTHER,
COST_TOTAL,
NUM_Cost
};
const RString COST_LABELS[] = {
"DOUBLESTEP",
"BRACKETJACK",
"JACK",
"JUMP",
"BRACKETTAP",
"HOLDSWITCH",
"MINE",
"FOOTSWITCH",
"MISSED_FOOTSWITCH",
"FACING",
"DISTANCE",
"SPIN",
"SIDESWITCH",
"CROWDED_BRACKET",
"OTHER",
"TOTAL"
};
struct StagePoint {
float x;
float y;
};
/// @brief A vector of StagePoints, which represents the
/// relative position of each arrow on the dance stage.
typedef std::vector<StagePoint> StageLayout;
/// @brief A vector of Foot values, which represents the player's
/// foot placement on the dance stage.
typedef std::vector<Foot> FootPlacement;
/// @brief Represents a specific possible state of the player's position
/// for a given row of the step chart.
struct State {
FootPlacement columns; // The position of the player
FootPlacement movedFeet; // Any feet that have moved from the previous state to this one
FootPlacement holdFeet; // Any feet that stayed in place due to a hold/roll note.
float second; // The time of the song represented by this state
int rowIndex; // The index of the row represented by this state
int idx;
int whereTheFeetAre[NUM_Foot]; // the inverse of columns
bool didTheFootMove[NUM_Foot]; // the inverse of movedFeet
bool isTheFootHolding[NUM_Foot]; //inverse of holdFeet
// These hashes are used in operator<() to speed up the comparison of the vectors.
// Their values are computed by calculateHashes(), which is used in StepParityGenerator::buildStateGraph().
int columnsHash = 0;
int movedFeetHash = 0;
int holdFeetHash = 0;
State()
{
State(4);
}
State(int columnCount)
{
columns = FootPlacement(columnCount, NONE);
movedFeet = FootPlacement(columnCount, NONE);
holdFeet = FootPlacement(columnCount, NONE);
second = 0;
rowIndex = 0;
idx = -1;
for(int i = 0; i < NUM_Foot; i++)
{
whereTheFeetAre[i] = -1;
didTheFootMove[i] = false;
isTheFootHolding[i] = false;
}
}
Json::Value ToJson(bool useStrings);
bool operator==(const State& other) const;
bool operator<(const State& other) const;
void calculateHashes();
};
/// @brief A convenience struct used to encapsulate data from NoteData in an
/// easier to work with format.
struct IntermediateNoteData {
TapNoteType type = TapNoteType_Empty; // type of the note
TapNoteSubType subtype = TapNoteSubType_Invalid;
int col = 0; // column/track number
int row = 0; // row on which the note occurs
float beat = 0; // beat on which the note occurs
float hold_length = 0; // If type is TapNoteType_HoldTail, length of hold, in beats
bool warped = false; // Is this note warped?
bool fake = false; // Is this note fake (besides being TapNoteType_Fake)?
float second = false; // time into the song on which the note occurs
Foot parity = NONE; // Which foot (and which part of the foot) will most likely be used
Json::Value ToJson(bool useStrings);
};
/// @brief A slightly complicated structure to encapsulate all of the data for a given
/// row of a step chart.
/// 'notes' and 'holds' will always have 'columnCount' entries. "Empty" columns will have a type of TapNoteType_Empty.
/// This shouldn't be confused with the idea of "rows" elsewhere in SM. Here, we only use
/// these Rows to represent a row that isn't empty.
struct Row {
std::vector<IntermediateNoteData> notes; // notes for the given row
std::vector<IntermediateNoteData> holds; // Any active hold notes, including ones that started before this row
std::set<int> holdTails; // Column index of any holds that end on this row
std::vector<float> mines; // If a mine occurred either on this row, or on a row on its own immediately
// preceding this one, the time of when that mine occurred, indexed by column.
std::vector<float> fakeMines; // The same thing, but for fake mines
float second = 0;
float beat = 0;
int rowIndex = 0;
int columnCount = 0;
Row()
{
Row(0);
}
Row(int _columnCount)
{
columnCount = _columnCount;
notes = std::vector<IntermediateNoteData>(columnCount);
holds = std::vector<IntermediateNoteData>(columnCount);
holdTails.clear();
mines = std::vector<float>(columnCount, 0);
fakeMines = std::vector<float>(columnCount, 0);
second = 0;
beat = 0;
rowIndex = 0;
}
Json::Value ToJson(bool useStrings);
static Json::Value ToJsonRows(const std::vector<Row> & rows, bool useStrings);
static Json::Value ParityRowsJson(const std::vector<Row> & rows);
};
/// @brief A counter used while creating rows
struct RowCounter
{
std::vector<IntermediateNoteData> notes; // Notes for the "current" row being generated
std::vector<IntermediateNoteData> activeHolds; // Any holds that are active for the current row
float lastColumnSecond = CLM_SECOND_INVALID;
float lastColumnBeat = CLM_SECOND_INVALID;
std::vector<float> mines; // The time at which a mine occurred for the current row,
// indexed by column
std::vector<float> fakeMines; // The time at which a fake mine occurred for the current row,
// indexed by column
std::vector<float> nextMines; // The time at which a mine occurred in the _previous_ row,
// indexed by column
std::vector<float> nextFakeMines; // The time at which a fake mine occurred in the _previous_ row,
// indexed by column
int noteCount = 0; // number of "notes" added to the counter for the current row.
RowCounter(int columnCount)
{
notes = std::vector<IntermediateNoteData>(columnCount);
activeHolds = std::vector<IntermediateNoteData>(columnCount);
mines = std::vector<float>(columnCount, 0);
fakeMines = std::vector<float>(columnCount, 0);
nextMines = std::vector<float>(columnCount, 0);
nextFakeMines = std::vector<float>(columnCount, 0);
lastColumnSecond = CLM_SECOND_INVALID;
lastColumnBeat = CLM_SECOND_INVALID;
}
};
/// @brief A node within a StepParityGraph.
/// Represents a given state, and its connections to the states in the
/// following row of the step chart.
struct StepParityNode
{
int id = 0; // The index of this node in its graph
State state;
std::unordered_map<StepParityNode *, float*> neighbors; // Connections to, and the cost of moving to, the connected nodes
StepParityNode(const State &_state)
{
state = _state;
}
Json::Value ToJson();
int neighborCount()
{
return static_cast<int>(neighbors.size());
}
};
/// @brief A comparator, needed in order use State objects as the key in a std::map.
struct StateComparator
{
bool operator()(const State& lhs, const State& rhs) const {
return lhs < rhs;
}
};
/// @brief A graph, representing all of the possible states for a step chart.
class StepParityGraph
{
private:
std::vector<StepParityNode *> nodes;
std::vector<State *> states;
std::vector<std::map<State, StepParityNode *, StateComparator>> stateNodeMap;
public:
StepParityNode * startNode; // This represents the very start of the song, before any notes
StepParityNode *endNode; // This represents the end of the song, after all of the notes
~StepParityGraph()
{
states.clear();
for(StepParityNode * node: nodes)
{
delete node;
}
}
/// @brief Returns a pointer to a StepParityNode that represents the given state within the graph.
/// If a node already exists, it is returned, otherwise a new one is created and added to the graph.
/// @param state
/// @return
StepParityNode *addOrGetExistingNode(const State &state);
void addEdge(StepParityNode* from, StepParityNode* to, float* costs) {
from->neighbors[to] = costs;
}
int nodeCount() const
{
return static_cast<int>(nodes.size());
}
Json::Value ToJson();
Json::Value NodeStateJson();
StepParityNode *operator[](int index) const
{
return nodes[index];
}
};
};
#endif
+532
View File
@@ -0,0 +1,532 @@
#include "global.h"
#include "StepParityGenerator.h"
#include "StepParityCost.h"
#include "NoteData.h"
#include "TechCounts.h"
#include "GameState.h"
// Generates foot parity given notedata
// Original algorithm by Jewel, polished by tillvit, then ported to C++
using namespace StepParity;
const std::map<StepsType, StageLayout> Layouts = {
{StepsType_dance_single, {
{0, 1}, // Left
{1, 0}, // Down
{1, 2}, // Up
{2, 1} // Right
}},
{StepsType_dance_double, {
{0, 1}, // P1 Left
{1, 0}, // P1 Down
{1, 2}, // P1 Up
{2, 1}, // P1 Right
{3, 1}, // P2 Left
{4, 0}, // P2 Down
{4, 2}, // P2 Up
{5, 1} // P2 Right
}}
};
void StepParityGenerator::analyzeNoteData(const NoteData &in, StepsType stepsType)
{
if(Layouts.find(stepsType) == Layouts.end())
{
LOG->Warn("Tried to call StepParityGenerator::analyze with an unsupported StepsType %s", StepsTypeToString(stepsType).c_str());
return;
}
layout = Layouts.at(stepsType);
columnCount = in.GetNumTracks();
CreateRows(in);
if(rows.size() == 0)
{
LOG->Trace("StepParityGenerator::analyze no rows, bailing out");
return;
}
buildStateGraph();
analyzeGraph();
}
void StepParityGenerator::analyzeGraph() {
nodes_for_rows = computeCheapestPath();
ASSERT_M(nodes_for_rows.size() == rows.size(), "nodes_for_rows should be the same length as rows!");
for (unsigned long i = 0; i < rows.size(); i++)
{
StepParityNode *node = graph[nodes_for_rows[i]];
for (int j = 0; j < rows[i].columnCount; j++) {
if(rows[i].notes[j].type != TapNoteType_Empty) {
rows[i].notes[j].parity = node->state.columns[j];
}
}
}
}
void StepParityGenerator::buildStateGraph()
{
// The first node of the graph is beginningState, which represents the time before
// the first note (and so it's roIndex is considered -1)
State beginningState(columnCount);
beginningState.rowIndex = -1;
beginningState.second = rows[0].second - 1;
StepParityNode *startNode = graph.addOrGetExistingNode(beginningState);
graph.startNode = startNode;
std::queue<State> previousStates;
previousStates.push(beginningState);
StepParityCost costCalculator(layout);
for (unsigned long i = 0; i < rows.size(); i++)
{
std::vector<State> uniqueStates;
Row &row = rows[i];
std::vector<FootPlacement> *PermuteFootPlacements = getFootPlacementPermutations(row);
while (!previousStates.empty())
{
State state = previousStates.front();
StepParityNode *initialNode = graph.addOrGetExistingNode(state);
for(auto it = PermuteFootPlacements->begin(); it != PermuteFootPlacements->end(); it++)
{
State resultState = initResultState(state, row, *it);
float* costs = costCalculator.getActionCost(&state, &resultState, rows, i);
resultState.calculateHashes();
StepParityNode *resultNode = graph.addOrGetExistingNode(resultState);
graph.addEdge(initialNode, resultNode, costs);
if(std::find(uniqueStates.begin(), uniqueStates.end(), resultState) == uniqueStates.end())
{
uniqueStates.push_back(resultState);
}
}
previousStates.pop();
}
for (State s : uniqueStates)
{
previousStates.push(s);
}
}
// at this point, previousStates holds all of the states for the very last row,
// which just get connected to the endState
State endState(columnCount);
endState.rowIndex = rows.size();
endState.second = rows[rows.size() - 1].second + 1;
StepParityNode *endNode = graph.addOrGetExistingNode(endState);
graph.endNode = endNode;
while(!previousStates.empty())
{
State state = previousStates.front();
StepParityNode *node = graph.addOrGetExistingNode(state);
float * emptyCosts = new float[NUM_Cost];
for(int i = 0; i < NUM_Cost; i++)
{
emptyCosts[i] = 0;
}
graph.addEdge(node, endNode, emptyCosts);
previousStates.pop();
}
}
State StepParityGenerator::initResultState(State &initialState, Row &row, const FootPlacement &columns)
{
State resultState(row.columnCount);
resultState.columns = columns;
resultState.rowIndex = row.rowIndex;
// I tried to condense this, but kept getting the logic messed up
for (unsigned long i = 0; i < columns.size(); i++)
{
if(columns[i] == NONE) {
continue;
}
resultState.whereTheFeetAre[columns[i]] = i;
if(row.holds[i].type == TapNoteType_Empty)
{
resultState.movedFeet[i] = columns[i];
resultState.didTheFootMove[columns[i]] = true;
continue;
}
if(initialState.columns[i] != columns[i])
{
resultState.movedFeet[i] = columns[i];
resultState.didTheFootMove[columns[i]] = true;
}
}
for (unsigned long i = 0; i < columns.size(); i++)
{
if(columns[i] == NONE) {
continue;
}
if(row.holds[i].type != TapNoteType_Empty)
{
resultState.holdFeet[i] = columns[i];
resultState.isTheFootHolding[columns[i]] = true;
}
}
resultState.second = row.second;
return resultState;
}
std::vector<FootPlacement>* StepParityGenerator::getFootPlacementPermutations(const Row &row)
{
int cacheKey = getPermuteCacheKey(row);
auto maybePermuteFootPlacements = permuteCache.find(cacheKey);
if (maybePermuteFootPlacements == permuteCache.end())
{
FootPlacement blankColumns(row.columnCount, NONE);
std::vector<FootPlacement> computedPermutations = PermuteFootPlacements(row, blankColumns, 0);
permuteCache[cacheKey] = std::move(computedPermutations);
}
return &permuteCache[cacheKey];
}
std::vector<FootPlacement> StepParityGenerator::PermuteFootPlacements(const Row &row, FootPlacement columns, unsigned long column)
{
if (column >= columns.size())
{
int leftHeelIndex = -1;
int leftToeIndex = -1;
int rightHeelIndex = -1;
int rightToeIndex = -1;
for (unsigned long i = 0; i < columns.size(); i++)
{
if (columns[i] == NONE)
continue;
if (columns[i] == LEFT_HEEL)
leftHeelIndex = i;
if (columns[i] == LEFT_TOE)
leftToeIndex = i;
if (columns[i] == RIGHT_HEEL)
rightHeelIndex = i;
if (columns[i] == RIGHT_TOE)
rightToeIndex = i;
}
if (
(leftHeelIndex == -1 && leftToeIndex != -1) ||
(rightHeelIndex == -1 && rightToeIndex != -1))
{
return std::vector<FootPlacement>();
}
if (leftHeelIndex != -1 && leftToeIndex != -1)
{
if (!bracketCheck(leftHeelIndex, leftToeIndex))
return std::vector<FootPlacement>();
}
if (rightHeelIndex != -1 && rightToeIndex != -1)
{
if (!bracketCheck(rightHeelIndex, rightToeIndex))
return std::vector<FootPlacement>();
}
return {columns};
}
std::vector<FootPlacement> permutations;
if (row.notes[column].type != TapNoteType_Empty || row.holds[column].type != TapNoteType_Empty) {
for (StepParity::Foot foot: FEET) {
if(std::find(columns.begin(), columns.end(), foot) != columns.end())
{
continue;
}
FootPlacement newColumns = columns;
newColumns[column] = foot;
std::vector<FootPlacement> p = PermuteFootPlacements(row, newColumns, column + 1);
permutations.insert(permutations.end(), p.begin(), p.end());
}
return permutations;
}
return PermuteFootPlacements(row, columns, column + 1);
}
std::vector<int> StepParityGenerator::computeCheapestPath()
{
int start = graph.startNode->id;
int end = graph.endNode->id;
std::vector<int> shortest_path;
std::vector<float> cost(graph.nodeCount(), FLT_MAX);
std::vector<int> predecessor(graph.nodeCount(), -1);
cost[start] = 0;
for (int i = start; i <= end; i++)
{
StepParityNode *node = graph[i];
for(auto neighbor: node->neighbors)
{
int neighbor_id = neighbor.first->id;
float weight = neighbor.second[COST_TOTAL];
// printf("computeCheapestPath:: weight = %f", weight);
if(cost[i] + weight < cost[neighbor_id])
{
cost[neighbor_id] = cost[i] + weight;
predecessor[neighbor_id] = i;
}
}
}
int current_node = end;
while(current_node != start)
{
ASSERT_M(current_node != -1, "WHOA");
if(current_node != end)
{
shortest_path.push_back(current_node);
}
current_node = predecessor[current_node];
}
std::reverse(shortest_path.begin(), shortest_path.end());
return shortest_path;
}
void StepParityGenerator::CreateIntermediateNoteData(const NoteData &in, std::vector<IntermediateNoteData> &out)
{
TimingData *timing = GAMESTATE->GetProcessedTimingData();
int columnCount = in.GetNumTracks();
NoteData::all_tracks_const_iterator curr_note = in.GetTapNoteRangeAllTracks(0, MAX_NOTE_ROW);
std::vector<IntermediateNoteData> notes;
for (; !curr_note.IsAtEnd(); ++curr_note)
{
IntermediateNoteData note;
note.type = curr_note->type;
note.subtype = curr_note->subType;
note.col = curr_note.Track();
note.row = curr_note.Row();
note.beat = NoteRowToBeat(curr_note.Row());
note.second = timing->GetElapsedTimeFromBeat(note.beat);
note.fake = note.type == TapNoteType_Fake || timing->IsFakeAtBeat(note.row);
note.warped = timing->IsWarpAtRow(note.row);
if (note.type == TapNoteType_HoldHead)
{
note.hold_length = NoteRowToBeat(curr_note->iDuration);
}
else
{
note.hold_length = -1;
}
notes.push_back(note);
}
out.assign(notes.begin(), notes.end());
}
void StepParityGenerator::CreateRows(const NoteData &in)
{
TimingData *timing = GAMESTATE->GetProcessedTimingData();
int columnCount = in.GetNumTracks();
RowCounter counter = RowCounter(columnCount);
std::vector<IntermediateNoteData> noteData;
CreateIntermediateNoteData(in, noteData);
for (IntermediateNoteData note : noteData)
{
if (note.type == TapNoteType_Empty)
{
continue;
}
if (note.type == TapNoteType_Mine)
{
// If this mine occurs on the same row as everything else that's been counted
// (in other words, if this note doesn't represent the start of a new row),
// and this isn't the very first row, put it in nextMines??
// I honestly don't know why this works the way it does, it all feels
// really backwards to me.
// I think the complication comes from the fact that this is getting handled
// before checking whether or not this note represens a new row.
// But we only want to create a new Row if it has at least one note.
// So probably something like
/*
for(note of notes)
{
if(note is empty note)
{
continue
}
if(note is on new row and counter has at least one note)
{
create new row
reset counter
}
check if note is a mine or fake mine
if note is fake continue
put note into counter.notes
}
*/
if (note.second == counter.lastColumnSecond && rows.size() > 0)
{
if (note.fake)
{
counter.nextFakeMines[note.col] = note.second;
}
else
{
counter.nextMines[note.col] = note.second;
}
}
else
{
if (note.fake)
{
counter.fakeMines[note.col] = note.second;
}
else
{
counter.mines[note.col] = note.second;
}
}
continue;
}
if (note.fake)
{
continue;
}
if (counter.lastColumnSecond != note.second)
{
// we're past the previous row, so save all of the previous row's data
if (counter.lastColumnSecond != CLM_SECOND_INVALID)
{
AddRow(counter);
}
// Move mines and fakeMines to "next", and reset counters
counter.lastColumnSecond = note.second;
counter.lastColumnBeat = note.beat;
counter.nextMines.assign(counter.mines.begin(), counter.mines.end());
counter.nextFakeMines.assign(counter.fakeMines.begin(), counter.fakeMines.end());
counter.notes = std::vector<IntermediateNoteData>(columnCount);
counter.mines = std::vector<float>(columnCount);
counter.fakeMines = std::vector<float>(columnCount);
// reset any now-inactive holds to empty values
for (int c = 0; c < columnCount; c++)
{
if (counter.activeHolds[c].type == TapNoteType_Empty || note.beat > counter.activeHolds[c].beat + counter.activeHolds[c].hold_length)
{
counter.activeHolds[c] = IntermediateNoteData();
}
}
}
counter.notes[note.col] = note;
if (note.type == TapNoteType_HoldHead)
{
counter.activeHolds[note.col] = note;
}
}
AddRow(counter);
}
void StepParityGenerator::AddRow(RowCounter &counter)
{
Row newRow = CreateRow(counter);
newRow.rowIndex = rows.size();
rows.push_back(newRow);
}
Row StepParityGenerator::CreateRow(RowCounter &counter)
{
Row row = Row(columnCount);
row.notes.assign(counter.notes.begin(), counter.notes.end());
row.mines.assign(counter.nextMines.begin(), counter.nextMines.end());
row.fakeMines.assign(counter.nextFakeMines.begin(), counter.nextFakeMines.end());
row.second = counter.lastColumnSecond;
row.beat = counter.lastColumnBeat;
for (int c = 0; c < columnCount; c++)
{
// save any active holds
if (counter.activeHolds[c].type == TapNoteType_Empty || counter.activeHolds[c].second >= counter.lastColumnSecond)
{
row.holds[c] = IntermediateNoteData();
}
else
{
row.holds[c] = counter.activeHolds[c];
}
// save any hold tails
if (counter.activeHolds[c].type != TapNoteType_Empty)
{
if (abs(counter.activeHolds[c].beat + counter.activeHolds[c].hold_length - counter.lastColumnBeat) < 0.0005)
{
row.holdTails.insert(c);
}
}
}
return row;
}
int StepParityGenerator::getPermuteCacheKey(const Row &row)
{
int key = 0;
for (unsigned long i = 0; i < row.notes.size() && i < row.holds.size(); i++)
{
if(row.notes[i].type != TapNoteType_Empty || row.holds[i].type != TapNoteType_Empty)
{
key += pow(2, i);
}
}
return key;
}
Json::Value StepParityGenerator::SMEditorParityJson()
{
Json::Value root;
for (unsigned long i = 0; i < nodes_for_rows.size(); i++)
{
StepParityNode *node = graph[nodes_for_rows[i]];
root.append(node->state.ToJson(false));
}
return root;
}
bool StepParityGenerator::bracketCheck(int column1, int column2)
{
StagePoint p1 = layout[column1];
StagePoint p2 = layout[column2];
return getDistanceSq(p1, p2) <= 2;
}
float StepParityGenerator::getDistanceSq(StepParity::StagePoint p1, StepParity::StagePoint p2)
{
return (p1.y - p2.y) * (p1.y - p2.y) + (p1.x - p2.x) * (p1.x - p2.x);
}
+85
View File
@@ -0,0 +1,85 @@
#ifndef STEP_PARITY_GENERATOR_H
#define STEP_PARITY_GENERATOR_H
#include "GameConstantsAndTypes.h"
#include "NoteData.h"
#include "StepParityDatastructs.h"
#include <queue>
#include <unordered_map>
#include "json/json.h"
namespace StepParity {
/// @brief This class handles most of the work for generating step parities for a step chart.
class StepParityGenerator
{
private:
StageLayout layout;
std::map < int, std::vector<std::vector<StepParity::Foot>>> permuteCache;
public:
StepParityGraph graph;
std::vector<Row> rows;
std::vector<int> nodes_for_rows;
int columnCount;
/// @brief Analyzes the given NoteData to generate a vector of StepParity::Rows, with each step annotated with
/// a foot placement.
/// @param in The NoteData to analyze
/// @param stepsTypeStr StepsType, currently only supports "dance-single"
void analyzeNoteData(const NoteData &in, StepsType stepsType);
/// @brief Analyzes the given graph to find the least costly path from the beginnning to the end of the stepchart.
/// Sets the `parity` for the relevant notes of each row in rows.
void analyzeGraph();
/// @brief Generates a StepParityGraph from the given vector of Rows.
/// The graph inserts two additional nodes: one that represent the beginning of the song, before the first note,
/// and one that represents the end of the song, after the final note.
void buildStateGraph();
/// @brief Creates a new State, which is the result of moving from the given initialState
/// to the steps of the given row with the given foot placements in columns.
/// @param initialState The state of the player prior to the next row
/// @param row The next row for the resulting state
/// @param columns The foot placement for the resulting state
/// @return The resulting state
State initResultState(State &initialState, Row &row, const FootPlacement &columns);
/// @brief Returns a pointer to a vector of foot possible foot placements for the given row.
/// Utilizes the permuteCache to re-use vectors. The returned pointer points to a vector within the permuteCache.
/// @param row The row to calculate foot placement permutations for.
/// @return A pointer to a vector of foot placements.
std::vector<FootPlacement>* getFootPlacementPermutations(const Row &row);
/// @brief A recursive function that generates a vector of possible foot placements for the given row.
/// This function should not be used directly, instead use getFootPlacementPermutations().
/// @param row
/// @param columns
/// @param column
/// @return
std::vector<FootPlacement> PermuteFootPlacements(const Row &row, FootPlacement columns, unsigned long column);
/// @brief Computes the "cheapest" path through the given graph.
/// This relies on the fact that the nodes stored in the graph are topologically sorted (that is, all
/// of the nodes are ordered in such a way that each node comes before all the nodes it points to.)
/// This allows us to find the cheapest path in a single pass.
/// The resulting path includes one node for each row of the stepchart represented by the graph.
/// Returns a vector of node indices, which can be mapped back to the cheapest state for each row.
/// @return A vector of node indices, making up the cheapest path through the step chart.
std::vector<int> computeCheapestPath();
/// @brief Converts NoteData into an intermediate form that's a little more convenient
/// to work with when creating rows.
void CreateIntermediateNoteData(const NoteData &in, std::vector<IntermediateNoteData> &out);
void CreateRows(const NoteData &in);
void AddRow(RowCounter &counter);
Row CreateRow(RowCounter &counter);
int getPermuteCacheKey(const Row &row);
bool bracketCheck(int column1, int column2);
float getDistanceSq(StepParity::StagePoint p1, StepParity::StagePoint p2);
Json::Value SMEditorParityJson();
};
};
#endif
+258
View File
@@ -0,0 +1,258 @@
#include "global.h"
#include "TechCounts.h"
#include "NoteData.h"
#include "RageLog.h"
#include "LocalizedString.h"
#include "LuaBinding.h"
#include "TimingData.h"
#include "GameState.h"
#include "RageTimer.h"
static const char *TechCountsCategoryNames[] = {
"Crossovers",
"Footswitches",
"Sideswitches",
"Jacks",
"Brackets",
"Doublesteps"
};
XToString( TechCountsCategory );
XToLocalizedString( TechCountsCategory );
LuaFunction(TechCountsCategoryToLocalizedString, TechCountsCategoryToLocalizedString(Enum::Check<TechCountsCategory>(L, 1)) );
LuaXType( TechCountsCategory );
// TechCounts methods
TechCounts::TechCounts()
{
MakeUnknown();
}
void TechCounts::MakeUnknown()
{
FOREACH_ENUM( TechCountsCategory, rc )
{
(*this)[rc] = TECHCOUNTS_VAL_UNKNOWN;
}
}
void TechCounts::Zero()
{
FOREACH_ENUM( TechCountsCategory, rc )
{
(*this)[rc] = 0;
}
}
RString TechCounts::ToString( int iMaxValues ) const
{
if( iMaxValues == -1 )
iMaxValues = NUM_TechCountsCategory;
iMaxValues = std::min( iMaxValues, (int)NUM_TechCountsCategory );
std::vector<RString> asTechCounts;
for( int r=0; r < iMaxValues; r++ )
{
asTechCounts.push_back(ssprintf("%.3f", (*this)[r]));
}
return join( ",",asTechCounts );
}
void TechCounts::FromString( RString sTechCounts )
{
std::vector<RString> saValues;
split( sTechCounts, ",", saValues, true );
if( saValues.size() != NUM_TechCountsCategory )
{
MakeUnknown();
return;
}
FOREACH_ENUM( RadarCategory, rc )
{
(*this)[rc] = StringToFloat(saValues[rc]);
}
}
void TechCounts::CalculateTechCountsFromRows(const std::vector<StepParity::Row> &rows, TechCounts &out)
{
// arrays to hold the column for each Foot enum.
// A value of -1 means that Foot is not on any column
int previousFootPlacement[StepParity::NUM_Foot];
int currentFootPlacement[StepParity::NUM_Foot];
for (int f = 0; f < StepParity::NUM_Foot; f++)
{
previousFootPlacement[f] = -1;
currentFootPlacement[f] = -1;
}
// arrays to hold the foot placements for the current row and previos row.
// They're basically just so we don't have to reference currentRow.notes[c].parity everywhere
std::vector<StepParity::Foot> previousColumns(rows[0].columnCount, StepParity::NONE);
std::vector<StepParity::Foot> currentColumns(rows[0].columnCount, StepParity::NONE);
for (unsigned long i = 1; i < rows.size(); i++)
{
const StepParity::Row &currentRow = rows[i];
int noteCount = 0;
// copy the foot placement for the current row into currentColumns,
// and count up how many notes there are in this row
for (int c = 0; c < currentRow.columnCount; c++)
{
StepParity::Foot currFoot = currentRow.notes[c].parity;
TapNoteType currType = currentRow.notes[c].type;
// If this isn't either a tap or the beginning of a hold, skip it
if(currType != TapNoteType_Tap && currType != TapNoteType_HoldHead)
{
continue;
}
currentFootPlacement[currFoot] = c;
currentColumns[c] = currFoot;
noteCount += 1;
}
/*
Jacks are same arrow same foot
Doublestep is same foot on successive arrows
Brackets are jumps with one foot
Footswitch is different foot on the up or down arrow
Sideswitch is footswitch on left or right arrow
Crossovers are left foot on right arrow or vice versa
*/
// check for jacks and doublesteps
if(noteCount == 1)
{
for (StepParity::Foot foot: StepParity::FEET)
{
if(currentFootPlacement[foot] == -1 || previousFootPlacement[foot] == -1)
{
continue;
}
if(previousFootPlacement[foot] == currentFootPlacement[foot])
{
out[TechCountsCategory_Jacks] += 1;
}
else
{
out[TechCountsCategory_Doublesteps] += 1;
}
}
}
// check for brackets
if(noteCount >= 2)
{
if(currentFootPlacement[StepParity::LEFT_HEEL] != -1 && currentFootPlacement[StepParity::LEFT_TOE] != -1)
{
out[TechCountsCategory_Brackets] += 1;
}
if(currentFootPlacement[StepParity::RIGHT_HEEL] != -1 && currentFootPlacement[StepParity::RIGHT_TOE] != -1)
{
out[TechCountsCategory_Brackets] += 1;
}
}
// Check for footswitches, sideswitches, and crossovers
for (int c = 0; c < currentRow.columnCount; c++)
{
if(currentColumns[c] == StepParity::NONE)
{
continue;
}
// this same column was stepped on in the previous row, but not by the same foot ==> footswitch or sideswitch
if(previousColumns[c] != StepParity::NONE && previousColumns[c] != currentColumns[c])
{
// this is assuming only 4-panel single
if(c == 0 || c == 3)
{
out[TechCountsCategory_Sideswitches] += 1;
}
else
{
out[TechCountsCategory_Footswitches] += 1;
}
}
// if the right foot is pressing the left arrow, or the left foot is pressing the right ==> crossover
else if(c == 0 && previousColumns[c] == StepParity::NONE &&
(currentColumns[c] == StepParity::RIGHT_HEEL || currentColumns[c] == StepParity::RIGHT_TOE))
{
out[TechCountsCategory_Crossovers] += 1;
}
else if(c == 3 && previousColumns[c] == StepParity::NONE &&
(currentColumns[c] == StepParity::LEFT_HEEL || currentColumns[c] == StepParity::LEFT_TOE))
{
out[TechCountsCategory_Crossovers] += 1;
}
}
// Move the values from currentFootPlacement to previousFootPlacement,
// and reset currentFootPlacement
for (int f = 0; f < StepParity::NUM_Foot; f++)
{
previousFootPlacement[f] = currentFootPlacement[f];
currentFootPlacement[f] = -1;
}
for (int c = 0; c < currentRow.columnCount; c++)
{
previousColumns[c] = currentColumns[c];
currentColumns[c] = StepParity::NONE;
}
}
}
// lua start
class LunaTechCounts: public Luna<TechCounts>
{
public:
static int GetValue( T* p, lua_State *L ) { lua_pushnumber( L, (*p)[Enum::Check<TechCountsCategory>(L, 1)] ); return 1; }
LunaTechCounts()
{
ADD_METHOD( GetValue );
}
};
LUA_REGISTER_CLASS( TechCounts )
/*
* (c) 2023 Michael Votaw
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, and/or sell copies of the Software, and to permit persons to
* whom the Software is furnished to do so, provided that the above
* copyright notice(s) and this permission notice appear in all copies of
* the Software and that both the above copyright notice(s) and this
* permission notice appear in supporting documentation.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
* THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
+110
View File
@@ -0,0 +1,110 @@
#ifndef TECH_COUNTS_H
#define TECH_COUNTS_H
#include "GameConstantsAndTypes.h"
#include "StepParityGenerator.h"
class NoteData;
/** @brief Unknown radar values are given a default value. */
#define TECHCOUNTS_VAL_UNKNOWN -1
enum TechCountsCategory
{
TechCountsCategory_Crossovers = 0,
TechCountsCategory_Footswitches,
TechCountsCategory_Sideswitches,
TechCountsCategory_Jacks,
TechCountsCategory_Brackets,
TechCountsCategory_Doublesteps,
NUM_TechCountsCategory,
TechCountsCategory_Invalid
};
const RString& TechCountsCategoryToString( TechCountsCategory cat );
/**
* @brief Turn the radar category into a proper localized string.
* @param cat the radar category.
* @return the localized string version of the radar category.
*/
const RString& TechCountsCategoryToLocalizedString( TechCountsCategory cat );
LuaDeclareType( TechCountsCategory );
struct lua_State;
/** @brief Technical statistics */
struct TechCounts
{
private:
float m_Values[NUM_TechCountsCategory];
public:
float operator[](TechCountsCategory cat) const { return m_Values[cat]; }
float& operator[](TechCountsCategory cat) { return m_Values[cat]; }
float operator[](int cat) const { return m_Values[cat]; }
float& operator[](int cat) { return m_Values[cat]; }
TechCounts();
void MakeUnknown();
void Zero();
TechCounts& operator+=( const TechCounts& other )
{
FOREACH_ENUM( TechCountsCategory, tc )
{
(*this)[tc] += other[tc];
}
return *this;
}
bool operator==( const TechCounts& other ) const
{
FOREACH_ENUM( TechCountsCategory, tc )
{
if((*this)[tc] != other[tc])
{
return false;
}
}
return true;
}
bool operator!=( const TechCounts& other ) const
{
return !operator==( other );
}
RString ToString( int iMaxValues = -1 ) const; // default = all
void FromString( RString sValues );
void PushSelf( lua_State *L );
static void CalculateTechCountsFromRows(const std::vector<StepParity::Row> &rows, TechCounts &out);
};
#endif
/**
* @file
* @author Michael Votaw (c) 2023
* @section LICENSE
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, and/or sell copies of the Software, and to permit persons to
* whom the Software is furnished to do so, provided that the above
* copyright notice(s) and this permission notice appear in all copies of
* the Software and that both the above copyright notice(s) and this
* permission notice appear in supporting documentation.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
* THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/