From 6109105f972aa521b0fe5d91dee6e3b1ce6a23ba Mon Sep 17 00:00:00 2001 From: Michael Votaw Date: Tue, 23 Jul 2024 19:27:35 -0500 Subject: [PATCH] Added TechCounts and all of the StepParity classes --- src/CMakeData-data.cmake | 12 +- src/StepParityCost.cpp | 891 ++++++++++++++++++++++++++++++++++ src/StepParityCost.h | 81 ++++ src/StepParityDatastructs.cpp | 250 ++++++++++ src/StepParityDatastructs.h | 307 ++++++++++++ src/StepParityGenerator.cpp | 532 ++++++++++++++++++++ src/StepParityGenerator.h | 85 ++++ src/TechCounts.cpp | 258 ++++++++++ src/TechCounts.h | 110 +++++ 9 files changed, 2524 insertions(+), 2 deletions(-) create mode 100644 src/StepParityCost.cpp create mode 100644 src/StepParityCost.h create mode 100644 src/StepParityDatastructs.cpp create mode 100644 src/StepParityDatastructs.h create mode 100644 src/StepParityGenerator.cpp create mode 100644 src/StepParityGenerator.h create mode 100644 src/TechCounts.cpp create mode 100644 src/TechCounts.h diff --git a/src/CMakeData-data.cmake b/src/CMakeData-data.cmake index 2ef39dc020..cd1d0b533c 100644 --- a/src/CMakeData-data.cmake +++ b/src/CMakeData-data.cmake @@ -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 diff --git a/src/StepParityCost.cpp b/src/StepParityCost.cpp new file mode 100644 index 0000000000..133f45b58e --- /dev/null +++ b/src/StepParityCost.cpp @@ -0,0 +1,891 @@ +#include "global.h" +#include "StepParityCost.h" +#include "NoteData.h" +#include "TechCounts.h" +#include "GameState.h" + +using namespace StepParity; + + +template +bool vectorIncludes(const std::vector& vec, const T& value, int columnCount) { + for (int i = 0; i < columnCount; i++) + { + if(vec[i] == value) + { + return true; + } + } + return false; +} + +template +int indexOf(const std::vector& vec, const T& value, int columnCount) { + for (int i = 0; i < columnCount; i++) + { + if(vec[i] == value) + { + return i; + } + } + return -1; +} + +template +bool isEmpty(const std::vector & vec, int columnCount) { + for (int i = 0; i < columnCount; i++) + { + if(static_cast(vec[i]) != 0) + { + return false; + } + } + return true; +} + + +float* StepParityCost::getActionCost(State * initialState, State * resultState, std::vector& 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 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 & 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& 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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, + }; + } diff --git a/src/StepParityCost.h b/src/StepParityCost.h new file mode 100644 index 0000000000..ee331d2617 --- /dev/null +++ b/src/StepParityCost.h @@ -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 &rows, int rowIndex); + + private: + void mergeInitialAndResultPosition(State * initialState, State * resultState, std::vector &combinedColumns, int columnCount); + float calcMineCost(State * initialState, State * resultState, Row &row, std::vector &combinedColumns, int columnCount); + float calcHoldSwitchCost(State * initialState, State * resultState, Row &row, std::vector &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 &rows, int rowIndex, bool movedLeft, bool movedRight, bool jackedLeft, bool jackedRight, bool didJump, int columnCount); + float calcDoublestepCost(State * initialState, State * resultState, std::vector & 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 &combinedColumns, int columnCount); + float calcSpinCosts(State * initialState, State * resultState, std::vector & combinedColumns, int columnCount); + float caclFootswitchCost(State * initialState, State * resultState, Row &row, std::vector &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 &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 + diff --git a/src/StepParityDatastructs.cpp b/src/StepParityDatastructs.cpp new file mode 100644 index 0000000000..c737982999 --- /dev/null +++ b/src/StepParityDatastructs.cpp @@ -0,0 +1,250 @@ +#include "global.h" +#include "StepParityDatastructs.h" + +using namespace StepParity; + + +// +// Graph/Node methods +// +int calculateVectorHash(const std::vector &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(stateNodeMap.size()) <= rowIndex) + { + stateNodeMap.push_back(std::map()); + } + + 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 +Json::Value FeetToJson(const Container& feets, bool useStrings) +{ + Json::Value root; + for(Foot f: feets) + { + if(useStrings) + { + root.append(FEET_LABELS[static_cast(f)]); + } + else + { + root.append(static_cast(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(type)]; + root["subtype"] = TapNoteSubTypeShortNames[static_cast(subtype)]; + root["parity"] = FEET_LABELS[static_cast(parity)]; + + } + else + { + root["type"] = static_cast(type); + root["subtype"] = static_cast(subtype); + root["parity"] = static_cast(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 & rows, bool useStrings) +{ + Json::Value root; + for(Row row: rows) + { + root.append(row.ToJson(useStrings)); + } + return root; +} + +Json::Value Row::ParityRowsJson(const std::vector & rows) +{ + Json::Value root; + for(Row row: rows) + { + Json::Value pr; + for(IntermediateNoteData note: row.notes) + { + pr.append(static_cast(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; +} diff --git a/src/StepParityDatastructs.h b/src/StepParityDatastructs.h new file mode 100644 index 0000000000..546d18c6f7 --- /dev/null +++ b/src/StepParityDatastructs.h @@ -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 +#include + + +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 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 StageLayout; + + /// @brief A vector of Foot values, which represents the player's + /// foot placement on the dance stage. + typedef std::vector 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 notes; // notes for the given row + std::vector holds; // Any active hold notes, including ones that started before this row + std::set holdTails; // Column index of any holds that end on this row + std::vector 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 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(columnCount); + holds = std::vector(columnCount); + holdTails.clear(); + mines = std::vector(columnCount, 0); + fakeMines = std::vector(columnCount, 0); + second = 0; + beat = 0; + rowIndex = 0; + } + + Json::Value ToJson(bool useStrings); + static Json::Value ToJsonRows(const std::vector & rows, bool useStrings); + static Json::Value ParityRowsJson(const std::vector & rows); + }; + + /// @brief A counter used while creating rows + struct RowCounter + { + std::vector notes; // Notes for the "current" row being generated + std::vector activeHolds; // Any holds that are active for the current row + float lastColumnSecond = CLM_SECOND_INVALID; + float lastColumnBeat = CLM_SECOND_INVALID; + + std::vector mines; // The time at which a mine occurred for the current row, + // indexed by column + std::vector fakeMines; // The time at which a fake mine occurred for the current row, + // indexed by column + + std::vector nextMines; // The time at which a mine occurred in the _previous_ row, + // indexed by column + std::vector 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(columnCount); + activeHolds = std::vector(columnCount); + mines = std::vector(columnCount, 0); + fakeMines = std::vector(columnCount, 0); + nextMines = std::vector(columnCount, 0); + nextFakeMines = std::vector(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 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(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 nodes; + std::vector states; + std::vector> 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(nodes.size()); + } + + Json::Value ToJson(); + Json::Value NodeStateJson(); + StepParityNode *operator[](int index) const + { + return nodes[index]; + } + }; + + +}; + +#endif diff --git a/src/StepParityGenerator.cpp b/src/StepParityGenerator.cpp new file mode 100644 index 0000000000..cdd9be58e1 --- /dev/null +++ b/src/StepParityGenerator.cpp @@ -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 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 previousStates; + previousStates.push(beginningState); + StepParityCost costCalculator(layout); + + for (unsigned long i = 0; i < rows.size(); i++) + { + std::vector uniqueStates; + Row &row = rows[i]; + std::vector *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* 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 computedPermutations = PermuteFootPlacements(row, blankColumns, 0); + permuteCache[cacheKey] = std::move(computedPermutations); + } + return &permuteCache[cacheKey]; +} + + +std::vector 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(); + } + if (leftHeelIndex != -1 && leftToeIndex != -1) + { + if (!bracketCheck(leftHeelIndex, leftToeIndex)) + return std::vector(); + } + if (rightHeelIndex != -1 && rightToeIndex != -1) + { + if (!bracketCheck(rightHeelIndex, rightToeIndex)) + return std::vector(); + } + return {columns}; + } + + std::vector 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 p = PermuteFootPlacements(row, newColumns, column + 1); + permutations.insert(permutations.end(), p.begin(), p.end()); + } + return permutations; + } + return PermuteFootPlacements(row, columns, column + 1); +} + + +std::vector StepParityGenerator::computeCheapestPath() +{ + int start = graph.startNode->id; + int end = graph.endNode->id; + std::vector shortest_path; + std::vector cost(graph.nodeCount(), FLT_MAX); + std::vector 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 &out) +{ + TimingData *timing = GAMESTATE->GetProcessedTimingData(); + int columnCount = in.GetNumTracks(); + + NoteData::all_tracks_const_iterator curr_note = in.GetTapNoteRangeAllTracks(0, MAX_NOTE_ROW); + + std::vector 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 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(columnCount); + counter.mines = std::vector(columnCount); + counter.fakeMines = std::vector(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); +} diff --git a/src/StepParityGenerator.h b/src/StepParityGenerator.h new file mode 100644 index 0000000000..97fdbe4e11 --- /dev/null +++ b/src/StepParityGenerator.h @@ -0,0 +1,85 @@ +#ifndef STEP_PARITY_GENERATOR_H +#define STEP_PARITY_GENERATOR_H + +#include "GameConstantsAndTypes.h" +#include "NoteData.h" +#include "StepParityDatastructs.h" +#include +#include +#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>> permuteCache; + + public: + StepParityGraph graph; + std::vector rows; + std::vector 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* 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 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 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 &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 diff --git a/src/TechCounts.cpp b/src/TechCounts.cpp new file mode 100644 index 0000000000..2cb2c899d3 --- /dev/null +++ b/src/TechCounts.cpp @@ -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(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 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 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 &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 previousColumns(rows[0].columnCount, StepParity::NONE); + std::vector 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 +{ +public: + + + static int GetValue( T* p, lua_State *L ) { lua_pushnumber( L, (*p)[Enum::Check(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. + */ diff --git a/src/TechCounts.h b/src/TechCounts.h new file mode 100644 index 0000000000..93d4067718 --- /dev/null +++ b/src/TechCounts.h @@ -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 &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. + */