From c35bc34aaf43c005b13e17d20adabb0087c2404d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eke=20P=C3=A9ter?= Date: Wed, 23 Aug 2023 19:40:39 +0200 Subject: [PATCH] Fix random map choosing same maps --- .../{MapListReader.uc => FsMapsReader.uc} | 2 +- MVES/Classes/MV_MapList.uc | 155 +++++++++++------- MVES/Classes/MapListDecoder.uc | 121 ++++++++++++++ MVES/Classes/MapVote.uc | 28 +++- MVES/Classes/MapVoteResult.uc | 51 ++++++ TestMVE/Classes/TestMapListEncoder.uc | 63 +++++-- TestMVE/Classes/TestMapVoteResult.uc | 2 + 7 files changed, 344 insertions(+), 78 deletions(-) rename MVES/Classes/{MapListReader.uc => FsMapsReader.uc} (91%) create mode 100644 MVES/Classes/MapListDecoder.uc diff --git a/MVES/Classes/MapListReader.uc b/MVES/Classes/FsMapsReader.uc similarity index 91% rename from MVES/Classes/MapListReader.uc rename to MVES/Classes/FsMapsReader.uc index 3618d88..214085c 100644 --- a/MVES/Classes/MapListReader.uc +++ b/MVES/Classes/FsMapsReader.uc @@ -1,4 +1,4 @@ -class MapListReader extends Info; +class FsMapsReader extends Info; var int iSeek; var String FirstMap; diff --git a/MVES/Classes/MV_MapList.uc b/MVES/Classes/MV_MapList.uc index c200510..65078b0 100644 --- a/MVES/Classes/MV_MapList.uc +++ b/MVES/Classes/MV_MapList.uc @@ -34,12 +34,12 @@ var config string M[16384]; var string MapListString; //Send this over the net! var MapHistory History; -var MapListReader Reader; +var FsMapsReader Reader; event PostBeginPlay() { - History = new(self) class'MapHistory'; - History.MapList = self; + History = new(Self) class'MapHistory'; + History.MapList = Self; } //We scan all maps, check if they match our filters @@ -63,7 +63,7 @@ function GlobalLoad(bool bFullscan) Mutator.CleanRules(); Mutator.CountFilters(); if ( Mutator.ServerCodeName == '' ) - Mutator.SetPropertyText("ServerCodeName",string(rand(MaxInt)) $ string(rand(MaxInt)) ); + Mutator.SetPropertyText("ServerCodeName",string(rand(MaxInt))$string(rand(MaxInt)) ); CacheCodes(); iMapList = 0; @@ -77,13 +77,13 @@ function GlobalLoad(bool bFullscan) } if ( Mutator.HasRandom(i) ) { - CurRules = CurRules $ ":" $ TwoDigits(i); + CurRules = CurRules$":"$TwoDigits(i); } } if ( CurRules != "" ) { - MapList[iMapList] = "Random" $ CurRules $ ";"; + MapList[iMapList] = "Random"$CurRules$";"; iMapList ++ ; PrevRules = CurRules; } @@ -146,7 +146,7 @@ function GlobalLoad(bool bFullscan) iLen = Len( TmpCodes[i]); for ( j = FStart[i] ; j < FEnd[i] ; j ++ ) { - if ( ! (Left( Mutator.GetMapFilter(j), iLen) ~= TmpCodes[i]) ) //Check that this IS a filter for this gamemode + if (! (Left( Mutator.GetMapFilter(j), iLen) ~= TmpCodes[i]) ) //Check that this IS a filter for this gamemode continue; sTest = Mid( Mutator.GetMapFilter(j), iLen); if ( Mutator.bEnableMapTags && InStr(sTest, ":") == 0 ) //Tag match @@ -181,7 +181,7 @@ function GlobalLoad(bool bFullscan) } if ( bAddTag ) - CurRules = CurRules $ GameTags[i]; + CurRules = CurRules$GameTags[i]; } if ( CurRules != "" ) { @@ -190,11 +190,11 @@ function GlobalLoad(bool bFullscan) Maps[ iMaps ++ ] = ClearMap; if (CurRules == PrevRules) { - MapList[ iMapList ] = ClearMap $ ";"; + MapList[ iMapList ] = ClearMap$";"; } else { - MapList[ iMapList ] = ClearMap $ CurRules $ ";"; + MapList[ iMapList ] = ClearMap$CurRules$";"; } iMapList ++ ; PrevRules = CurRules; @@ -204,15 +204,15 @@ function GlobalLoad(bool bFullscan) Log("[MVE] Remove old + add new maps..."); NewMaps = ":"; for (i = 0; i < iMaps; i ++ ) - NewMaps = NewMaps $ Maps[i] $ ":"; + NewMaps = NewMaps$Maps[i]$":"; ClearMap = ":"; j = 0; for (i = 0; i < iM; i ++ ) { - if (InStr(NewMaps, ":" $ M[i] $ ":") != -1) + if (InStr(NewMaps, ":"$M[i]$":") != -1) { - ClearMap = ClearMap $ M[i] $ ":"; + ClearMap = ClearMap$M[i]$":"; if (i != j) M[j] = M[i]; j ++ ; @@ -222,7 +222,7 @@ function GlobalLoad(bool bFullscan) for (i = 0; i < iMaps; i ++ ) { - if (InStr(ClearMap, ":" $ Maps[i] $ ":") == -1) + if (InStr(ClearMap, ":"$Maps[i]$":") == -1) M[j ++ ] = Maps[i]; } iM = j; @@ -231,12 +231,12 @@ function GlobalLoad(bool bFullscan) ClearMap = ":"; for (i = 0; i < iMapList; i ++ ) - ClearMap = ClearMap $ Left(MapList[i], InStr(MapList[i], ":")) $ ":" $ i $ ":"; + ClearMap = ClearMap$Left(MapList[i], InStr(MapList[i], ":"))$":"$i$":"; for (j = 0; j < ArrayCount(iNewMaps) && iM - j > 0; j ++ ) { NewMaps = M[iM - j - 1]; - k = InStr(ClearMap, ":" $ NewMaps $ ":"); + k = InStr(ClearMap, ":"$NewMaps$":"); if (k == -1) { iNewMaps[j] = 0; @@ -258,11 +258,11 @@ function GlobalLoad(bool bFullscan) for ( j = FStart[i] ; j < FEnd[i] ; j ++ ) { //Check that this IS a filter for this gamemode - if ( ! (Left( Mutator.GetMapFilter(j), iLen) ~= TmpCodes[i]) ) + if (! (Left( Mutator.GetMapFilter(j), iLen) ~= TmpCodes[i]) ) { continue; } - MapList[iMapList] = Mid( Mutator.GetMapFilter(j), iLen) $ GameTags[i] $ ";"; + MapList[iMapList] = Mid( Mutator.GetMapFilter(j), iLen)$GameTags[i]$";"; iMapList ++ ; } } @@ -330,18 +330,18 @@ function CacheCodes() for ( i = 0 ; i < Mutator.iGames ; i ++ ) { - tmpCode = Mutator.MutatorCode(i) $ " "; + tmpCode = Mutator.MutatorCode(i)$" "; if ( tmpCode != " " ) { for ( j = 0 ; j < k ; j ++ ) if ( TmpCodes[j] == tmpCode ) { - GameTags[j] = GameTags[j] $ ":" $ TwoDigits(i); + GameTags[j] = GameTags[j]$":"$TwoDigits(i); goto END_LOOP; } if ( Left(tmpCode,7) ~= "premade" ) IsPremade[k] = 1; - GameTags[k] = ":" $ TwoDigits(i); + GameTags[k] = ":"$TwoDigits(i); TmpCodes[k ++ ] = tmpCode; } END_LOOP: @@ -418,7 +418,7 @@ function EnumerateGames() if ( TmpGameName[j] ~= gameName ) { // add to existing rule from RuleList - RuleList[j] = RuleList[j] $ ":" $ TwoDigits(i); + RuleList[j] = RuleList[j]$":"$TwoDigits(i); found = True; } } @@ -426,7 +426,7 @@ function EnumerateGames() { // add new entry into RuleList TmpGameName[iRules] = gameName; - RuleList[iRules] = ":" $ TwoDigits(i); + RuleList[iRules] = ":"$TwoDigits(i); iRules ++ ; } } @@ -483,7 +483,7 @@ function bool IsValidMap( out string MapString, out string reason ) { // found! // normalize string for voting stage - MapString = Left(MapList[i], iLen) $ ":" $ GameIdx; + MapString = Left(MapList[i], iLen)$":"$GameIdx; // TODO check if map on coldown // TODO check if map in crashed state return True; @@ -548,26 +548,26 @@ function GenerateString() local string S; for ( i = 0 ; i < iRules ; i ++ ) - MapListString = MapListString $ "RuleList[" $ string(i) $ "]=" $ RuleList[i] $ chr(13); + MapListString = MapListString$"RuleList["$string(i)$"]="$RuleList[i]$chr(13); for ( i = 0 ; i < iGameC ; i ++ ) { if ( GameNames[i] != "" ) { j ++ ; - MapListString = MapListString $ "GameModeName[" $ string(i) $ "]=" $ GameNames[i] $ chr(13) - $ "RuleName[" $ string(i) $ "]=" $ RuleNames[i] $ chr(13) - $ "VotePriority[" $ string(i) $ "]=" $ string(VotePriority[i]) $ chr(13); + MapListString = MapListString$"GameModeName["$string(i)$"]="$GameNames[i]$chr(13) + $"RuleName["$string(i)$"]="$RuleNames[i]$chr(13) + $"VotePriority["$string(i)$"]="$string(VotePriority[i])$chr(13); } } GameCount = j; //HACK FIX for ( i = 0 ; i < iMapList ; i ++ ) - MapListString = MapListString $ "MapList[" $ string(i) $ "]=" $ MapList[i] $ chr(13); + MapListString = MapListString$"MapList["$string(i)$"]="$MapList[i]$chr(13); MapListString = MapListString - $ "MapCount=" $ string(iMapList) $ chr(13) - $ "RuleListCount=" $ string(iRules) $ chr(13) - $ "RuleCount=" $ string(GameCount) $ chr(13); + $"MapCount="$string(iMapList)$chr(13) + $"RuleListCount="$string(iRules)$chr(13) + $"RuleCount="$string(GameCount)$chr(13); GenerateCode(); } @@ -589,7 +589,7 @@ function GenerateCode() } i ++ ; } - LastUpdate = class'MV_MainExtension'.static.NumberToByte(K) $ class'MV_MainExtension'.static.NumberToByte(j); + LastUpdate = class'MV_MainExtension'.static.NumberToByte(K)$class'MV_MainExtension'.static.NumberToByte(j); } function string GetStringSection( string StartsFrom) @@ -611,9 +611,9 @@ function string GetStringSection( string StartsFrom) Result = Left( Result, Len(Result) -1); if (!bNext ) - return "[START]" $ chr(13) $ Result $ "[END]" $ chr(13) $ "[NEXT]" $ chr(13); + return "[START]"$chr(13)$Result$"[END]"$chr(13)$"[NEXT]"$chr(13); //Notify END of list, add the "[X]" on individual map entries! - return "[START]" $ chr(13) $ Result $ "[NEXT]" $ chr(13) $ "[END]" $ chr(13); + return "[START]"$chr(13)$Result$"[NEXT]"$chr(13)$"[END]"$chr(13); } function string RemoveExtension( string aStr) @@ -639,15 +639,10 @@ function string RemoveExtension( string aStr) function string TwoDigits( int i) { if ( i < 10 ) - return "0"$ string(i); + return "0"$string(i); return string(i); } -final function string MapName( int i) -{ - return class'MV_MainExtension'.static.ByDelimiter(MapList[i],":"); -} - final function string MapGames( int i) { return Mid(MapList[i], InStr( MapList[i], ":") + 1); @@ -673,34 +668,74 @@ final function bool IsEdited( int i) return MapList[i] != default.MapList[i]; } -final function string RandomMap( int Game) +final function string RandomMap( int gameIdx, int forPlayerCount ) { - local int MaxRandom, i, iRandom; - local string GameString, Result; - - GameString = ":" $ TwoDigits( Game); - MaxRandom = 1; - for ( i = 1 ; i < iMapList ; i ++ ) + local MapListDecoder decoder; + local int i, j, code; + local string result; + local string options[4096]; + local string bestResult; + local string levelString; + local MapVoteResult map; + local int bestScore, resultScore; + local int count, idealPlayers, delta; + + decoder = new class'MapListDecoder'; + count = 0; + + for (i = 0; i < iMapList; i+=1) { - if ( (InStr( MapList[i], GameString) > 0) && (Left(MapList[i], 3) != "[X]") ) + decoder.L[i] = MapList[i]; + while (decoder.ReadEntry(result)) { - if ( Rand(MaxRandom ++ ) == 0 ) - Result = MapList[i]; + while (decoder.ReadCode(code)) { + if (code == gameIdx ) + { + options[count] = result; + count += 1; + continue; + } + } } } - if ( Result != "" ) - return class'MV_MainExtension'.static.ByDelimiter(Result,":"); - //All maps red? - MaxRandom = 1; - for ( i = 1 ; i < iMapList ; i ++ ) + + levelString = ""$decoder; + decoder.Parse(levelString, ".", levelString); + bestScore = -1000; + for (i = 0; i < 10; i += 1) { - if ( (InStr( MapList[i], GameString) > 0) ) + result = options[(Rand(count) + gameIdx * 7 + count * 13) % count]; + resultScore = 0; + map = class'MapVoteResult'.static.Create(result, gameIdx); + if (result ~= "Random") + { + resultScore -= 100; + } + else if (!map.CanMapBeLoaded()) + { + resultScore -= 100; + } + else if (forPlayerCount >= 0) + { + idealPlayers = map.GetAvgIdealPlayerCount(); + if (idealPlayers >= 0) + { + delta = forPlayerCount - idealPlayers; + if (delta < 0) + { + delta = -delta; + } + resultScore += 32 - delta; + } + } + if (result ~= levelString) resultScore -= 10; + if (bestScore <= resultScore) { - if ( Rand(MaxRandom ++ ) == 0 ) - Result = MapList[i]; + bestResult = result; + bestScore = resultScore; } } - return class'MV_MainExtension'.static.ByDelimiter(Result,":"); + return bestResult; } final simulated function float GetVotePriority( int Idx) diff --git a/MVES/Classes/MapListDecoder.uc b/MVES/Classes/MapListDecoder.uc new file mode 100644 index 0000000..526c231 --- /dev/null +++ b/MVES/Classes/MapListDecoder.uc @@ -0,0 +1,121 @@ +class MapListDecoder extends MV_Parser; + +var string L[4096]; + +// reader props +var int R; +var string ReadLine; +var string ReadMapEntry; +var string ReadMapName; +var string PrevReadCodes[8]; +var int PrevReadCodesAt; + +// writer props +var int P; +var int RangeStart; +var int PrevIndex; +var int CharsPerLine; +var int PredictedCode; +const InitialCharsPerLine = 500; +var string PrevCodes[8]; +var int PrevCodesIndex; +var string Codes; +var string NextMap; +var string NextMapEnc; +var string PrevMap; +const NA = -65536; + +function ResetReader() +{ + local int i; + R = 0; + ReadLine = EMPTY_STRING; + ReadMapEntry = EMPTY_STRING; + PrevReadCodesAt = 0; + for (i = 0; i < ArrayCount(PrevCodes); i+=1) + { + PrevCodes[i] = EMPTY_STRING; + } +} + +function bool ReadEntry(out string resultMap) +{ + local bool trimLegacySemicolon; + + if (ReadLine == "") + { + if (L[R] != "") + { + ReadLine = L[R]; + R += 1; + } + else + { + return False; + } + } + + // detect legacy semicolon mode + // workaround implemented on 2023-08-23 + trimLegacySemicolon = InStr(ReadMapEntry, "|") == -1; + + if (Parse(ReadMapEntry, "|", ReadLine)) + { + if (Parse(ReadMapName, ":", ReadMapEntry)) + { + resultMap = ReadMapName; + if (ReadMapEntry == EMPTY_STRING) + { + if (trimLegacySemicolon) + { + // map list is from version of mve which has suffixed semicolons + if (Right(resultMap, 1) == ";") + { + resultMap = Left(resultMap, Len(resultMap) -1); + } + } + // backreference to previous codes + ReadMapEntry = PrevReadCodes[(PrevReadCodesAt - 1) & 7]; + } + else if (Mid(ReadMapEntry, 0, 1) == "-") + { + // handle long backreference + ReadMapEntry = PrevReadCodes[(int(ReadMapEntry) -2) & 7]; + } + else + { + // new code definition + PrevReadCodes[PrevReadCodesAt] = ReadMapEntry; + PrevReadCodesAt = (PrevReadCodesAt + 1) & 7; + } + return True; + } + else + { + // ignore empty entry, reproduce with |||| or emtpy config lines + } + } + else + { + ReadEntry(resultMap); + } +} + +function bool ReadCode(out int code) +{ + local string str; + if (Parse(str, ":", ReadMapEntry)) + { + code = int(str); + return True; + } + else + { + return False; + } +} + +static function bool Parse(out string resultItem, string separator, out string mutableInput) +{ + return TrySplit(mutableInput, separator, resultItem, mutableInput); +} diff --git a/MVES/Classes/MapVote.uc b/MVES/Classes/MapVote.uc index 7ed0b1c..d68956c 100644 --- a/MVES/Classes/MapVote.uc +++ b/MVES/Classes/MapVote.uc @@ -279,7 +279,7 @@ event PostBeginPlay() } } MapList = new class'MV_MapList'; - MapList.Reader = Spawn(class'MapListReader'); + MapList.Reader = Spawn(class'FsMapsReader'); MapList.Mutator = self; if ( ExtensionClass != "" ) ExtensionC = class( DynamicLoadObject(ExtensionClass,class'class') ); @@ -732,7 +732,7 @@ function GenerateMapList(bool bFullscan) if ( MapList == none ) { MapList = new class'MV_MapList'; - MapList.Reader = Spawn(class'MapListReader'); + MapList.Reader = Spawn(class'FsMapsReader'); MapList.Mutator = self; } MapList.GlobalLoad(bFullscan); @@ -1365,6 +1365,24 @@ final function float VotePriority( int i) return CustomGame[i].VotePriority; } +final function int GetPlayerCount() +{ + local int count; + local Pawn p; + + count = 0; + + for (p = Level.PawnList; p != None; p = p.NextPawn) + { + if (PlayerPawn(p) != None && !p.IsA('Spectator')) + { + count += 1; + } + } + + return count; +} + final function bool CanVote(PlayerPawn Sender) { if (Sender.Player == None) @@ -1442,7 +1460,7 @@ function PrintCircularExtendsError(MapVoteResult r, int idx){ final function bool SetupTravelString( string mapStringWithIdx ) { local string spk, GameClassName, LogoTexturePackage, mapFileName, idxString; - local int idx, TickRate; + local int idx, TickRate, playerCount; local MV_MapOverrides MapOverrides; local MapVoteResult Result; local LevelInfo info; @@ -1457,8 +1475,8 @@ final function bool SetupTravelString( string mapStringWithIdx ) if ( Result.Map ~= "Random" ) { - // TODO idea random map should factor in number of players - Result.Map = MapList.RandomMap(Result.GameIndex); + PlayerCount = GetPlayerCount(); + Result.Map = MapList.RandomMap(Result.GameIndex, PlayerCount); } if (Result.CanMapBeLoaded() == false){ diff --git a/MVES/Classes/MapVoteResult.uc b/MVES/Classes/MapVoteResult.uc index e90a323..a72cede 100644 --- a/MVES/Classes/MapVoteResult.uc +++ b/MVES/Classes/MapVoteResult.uc @@ -239,6 +239,57 @@ function string GetSongString() return OriginalSong; } +function string GetIdealPlayerCountString() +{ + local string result; + result = GetLevelSummaryObject().IdealPlayerCount; + if (result == "") result = GetLevelInfoObject().IdealPlayerCount; + return result; +} + +function int GetAvgIdealPlayerCount() +{ + local int dashAt, temp, value, weight; + local string str; + str = GetIdealPlayerCountString(); + dashAt = InStr(str, "-"); + value = 0; + weight = 0; + if (dashAt >= 0) + { + temp = int(Left(str, dashAt)); + if (temp > 0) + { + value += temp; + weight += 1; + } + temp = int(Mid(str, dashAt + 1)); + if (temp > 0) + { + value += temp; + weight += 1; + } + } + else + { + temp = int(str); + if (temp > 0) + { + value += temp; + weight += 1; + } + } + if (value > 0 && weight > 0) + { + return value / weight; + } + else + { + // value not known + return -1; + } +} + function LoadSongInformation() { if (OriginalSong == "") diff --git a/TestMVE/Classes/TestMapListEncoder.uc b/TestMVE/Classes/TestMapListEncoder.uc index 673fe70..5c9bbe6 100644 --- a/TestMVE/Classes/TestMapListEncoder.uc +++ b/TestMVE/Classes/TestMapListEncoder.uc @@ -7,6 +7,8 @@ var int R; var string ReadLine; var string ReadMapEntry; var string ReadMapName; +var string PrevReadCodes[8]; +var int PrevReadCodesAt; // writer props var int P; @@ -74,27 +76,39 @@ function TestDecode() Describe("Read legacy ommited codes are repeated"); Reset(); - L[0] = "A:1:2|B|C"; + L[0] = "A:3:4:5|B"; ReadEntry(m); AssertEquals(m, "A", "first entry is A"); - ReadCode(c); AssertEquals(c, 1, "first code is 1"); - ReadCode(c); AssertEquals(c, 2, "second code is 2"); + ReadCode(c); AssertEquals(c, 3, "1st code is 3"); + ReadCode(c); AssertEquals(c, 4, "2nd code is 4"); + ReadCode(c); AssertEquals(c, 5, "3rd code is 5"); ReadEntry(m); AssertEquals(m, "B", "second entry is B"); - ReadCode(c); AssertEquals(c, 1, "first code is 1"); - ReadCode(c); AssertEquals(c, 2, "second code is 2"); - ReadEntry(m); AssertEquals(m, "C", "second entry is B"); - ReadCode(c); AssertEquals(c, 1, "first code is 1"); - ReadCode(c); AssertEquals(c, 2, "second code is 2"); + ReadCode(c); AssertEquals(c, 3, "1st code is 3"); + ReadCode(c); AssertEquals(c, 4, "2nd code is 4"); + ReadCode(c); AssertEquals(c, 5, "3rd code is 5"); + + Describe("Read legacy entry with semicolon"); + Reset(); + L[0] = "DM-Agony;"; + ReadEntry(m); AssertEquals(m, "DM-Agony", "legacy semicolon is trimmed"); } function ResetReader() { + local int i; R = 0; ReadLine = EMPTY_STRING; ReadMapEntry = EMPTY_STRING; + PrevReadCodesAt = 0; + for (i = 0; i < ArrayCount(PrevCodes); i+=1) + { + PrevCodes[i] = EMPTY_STRING; + } } function bool ReadEntry(out string resultMap) { + local bool trimLegacySemicolon; + if (ReadLine == "") { if (L[R] != "") @@ -107,26 +121,51 @@ function bool ReadEntry(out string resultMap) return False; } } + + // detect legacy semicolon mode + // workaround implemented on 2023-08-23 + trimLegacySemicolon = InStr(ReadMapEntry, "|") == -1; if (Parse(ReadMapEntry, "|", ReadLine)) { if (Parse(ReadMapName, ":", ReadMapEntry)) { resultMap = ReadMapName; + if (ReadMapEntry == EMPTY_STRING) + { + if (trimLegacySemicolon) + { + // map list is from version of mve which has suffixed semicolons + if (Right(resultMap, 1) == ";") + { + resultMap = Left(resultMap, Len(resultMap) -1); + } + } + // backreference to previous codes + ReadMapEntry = PrevReadCodes[(PrevReadCodesAt - 1) & 7]; + } + else if (Mid(ReadMapEntry, 0, 1) == "-") + { + // handle long backreference + ReadMapEntry = PrevReadCodes[(int(ReadMapEntry) -2) & 7]; + } + else + { + // new code definition + PrevReadCodes[PrevReadCodesAt] = ReadMapEntry; + PrevReadCodesAt = (PrevReadCodesAt + 1) & 7; + } return True; } else { - // repeat game list from prev map - resultMap = ReadMapEntry; - return True; + // ignore empty entry, reproduce with |||| or emtpy config lines } } else { ReadEntry(resultMap); } - } function bool ReadCode(out int code) diff --git a/TestMVE/Classes/TestMapVoteResult.uc b/TestMVE/Classes/TestMapVoteResult.uc index 87cdd25..0f93ed5 100644 --- a/TestMVE/Classes/TestMapVoteResult.uc +++ b/TestMVE/Classes/TestMapVoteResult.uc @@ -31,6 +31,8 @@ function TestMapLoading() s.Map = "DM-Fractal"; AssertEquals(s.CanMapBeLoaded(), True, "can be loaded"); AssertEquals(s.GetSongString(), "Mech8.Mech8", "song is mech8"); + AssertEquals(s.GetIdealPlayerCountString(), "2-4", "ideal player count string is 2-4"); + AssertEquals(s.GetAvgIdealPlayerCount(), 3, "parsed ideal player count is 3"); Describe("randomgarbage map"); s.Map = "randomgarbage";