Added TechCounts and all of the StepParity classes
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 ¤tRow = 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.
|
||||
*/
|
||||
@@ -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.
|
||||
*/
|
||||
Reference in New Issue
Block a user