Spike counter + caching for szy api + more badges

ToDo: save advanced stats to the DB, polish UI, add interactivity to graphs
This commit is contained in:
dan63047 2024-01-07 01:54:00 +03:00
parent 8dc2a5bced
commit 39569ffe0c
11 changed files with 102 additions and 50 deletions

View File

@ -5,6 +5,29 @@ import 'tetrio.dart';
// I want to implement those fancy TWC stats
// So, i'm going to read replay for things
int biggestSpikeFromReplay(events){
int previousIGEeventFrame = -1;
int spikeCounter = 0;
int biggestSpike = 0;
for (var event in events){
if (event["type"] == "ige" && event["data"]["data"]["type"] == "interaction"){
if (previousIGEeventFrame.isNegative){
spikeCounter = event['data']['data']['data']['amt'];
biggestSpike = spikeCounter;
}else{
if (event['data']['data']['frame'] - previousIGEeventFrame < 60){
spikeCounter = spikeCounter + event['data']['data']['data']['amt'] as int;
}else{
spikeCounter = event['data']['data']['data']['amt'];
}
biggestSpike = max(biggestSpike, spikeCounter);
}
previousIGEeventFrame = event['data']['data']['frame'];
}
}
return biggestSpike;
}
class Garbage{ // charsys where???
late int sent;
late int recived;
@ -43,11 +66,11 @@ class ReplayStats{
late int score;
late int topCombo;
late int topBtB;
late int topSpike;
late int tspins;
late Clears clears;
late Garbage garbage;
late Finesse finesse;
late int kills;
double get finessePercentage => finesse.perfectPieces / piecesPlaced;
@ -60,14 +83,14 @@ class ReplayStats{
required this.score,
required this.topCombo,
required this.topBtB,
required this.topSpike,
required this.tspins,
required this.clears,
required this.garbage,
required this.finesse,
required this.kills,
});
ReplayStats.fromJson(Map<String, dynamic> json){
ReplayStats.fromJson(Map<String, dynamic> json, int inputTopSpike){
seed = json['seed'];
linesCleared = json['lines'];
piecesPlaced = json['piecesplaced'];
@ -76,11 +99,11 @@ class ReplayStats{
score = json['score'];
topCombo = json['topcombo'];
topBtB = json['topbtb'];
topSpike = inputTopSpike;
tspins = json['tspins'];
clears = Clears.fromJson(json['clears']);
garbage = Garbage.fromJson(json['garbage']);
finesse = Finesse.fromJson(json['finesse']);
kills = json['kills'];
}
ReplayStats.createEmpty(){
@ -92,11 +115,11 @@ class ReplayStats{
score = 0;
topCombo = 0;
topBtB = 0;
topSpike = 0;
tspins = 0;
clears = Clears(singles: 0, doubles: 0, triples: 0, quads: 0, pentas: 0, allClears: 0, tSpinZeros: 0, tSpinSingles: 0, tSpinDoubles: 0, tSpinTriples: 0, tSpinPentas: 0, tSpinQuads: 0, tSpinMiniZeros: 0, tSpinMiniSingles: 0, tSpinMiniDoubles: 0);
garbage = Garbage(sent: 0, recived: 0, attack: 0, cleared: 0);
finesse = Finesse(combo: 0, faults: 0, perfectPieces: 0);
kills = 0;
}
ReplayStats operator + (ReplayStats other){
@ -108,11 +131,12 @@ class ReplayStats{
score: score + other.score,
topCombo: max(topCombo, other.topCombo),
topBtB: max(topBtB, other.topBtB),
topSpike: max(topSpike, other.topSpike),
tspins: tspins + other.tspins,
clears: clears + other.clears,
garbage: garbage + other.garbage,
finesse: finesse + other.finesse,
kills: kills + other.kills);
finesse: finesse + other.finesse
);
}
}
@ -122,6 +146,7 @@ class ReplayData{
late List<EndContextMulti> endcontext;
late List<List<ReplayStats>> stats;
late List<ReplayStats> totalStats;
late List<List<String>> roundWinners;
late List<int> roundLengths; // in frames
late int totalLength; // in frames
@ -130,6 +155,7 @@ class ReplayData{
required this.endcontext,
required this.stats,
required this.totalStats,
required this.roundWinners,
required this.roundLengths,
required this.totalLength
}){
@ -143,24 +169,20 @@ class ReplayData{
roundLengths = [];
totalLength = 0;
stats = [];
roundWinners = [];
totalStats = [ReplayStats.createEmpty(), ReplayStats.createEmpty()];
int firstInEndContext = json["data"][0]["board"].indexWhere((element) => element["id"] == endcontext[0].userId);
int secondInEndContext = json["data"][0]["board"].indexWhere((element) => element["id"] == endcontext[1].userId);
for(var round in json['data']) {
roundLengths.add(max(round['replays'][0]['frames'], round['replays'][1]['frames']));
totalLength = totalLength + max(round['replays'][0]['frames'], round['replays'][1]['frames']);
ReplayStats playerOne = ReplayStats.fromJson(round['replays'][0]['events'].last['data']['export']['stats']);
ReplayStats playerTwo = ReplayStats.fromJson(round['replays'][1]['events'].last['data']['export']['stats']);
int winner = round['board'].indexWhere((element) => element["success"] == true);
roundWinners.add([round['board'][winner]["id"], round['board'][winner]["username"]]);
ReplayStats playerOne = ReplayStats.fromJson(round['replays'][firstInEndContext]['events'].last['data']['export']['stats'], biggestSpikeFromReplay(round['replays'][secondInEndContext]['events'])); // (events contain recived attacks)
ReplayStats playerTwo = ReplayStats.fromJson(round['replays'][secondInEndContext]['events'].last['data']['export']['stats'], biggestSpikeFromReplay(round['replays'][firstInEndContext]['events']));
stats.add([playerOne, playerTwo]);
totalStats[0] = totalStats[0] + playerOne;
totalStats[1] = totalStats[1] + playerTwo;
// print(round['replays'][0]['events'].last['data']['export']['stats']);
// for (var event in round['replays'][0]['events']){
// if (event["type"] == "ige" && event["data"]["data"]["type"] == "interaction_confirm"){
// print(event['data']["data"]["data"]); // lol
// }
// }
}
// Сами по себе эти ивенты ничего не дают,
// Хотя можно попробовать вычислить biggest spike
}
}

View File

@ -65,6 +65,7 @@ class TetrioService extends DB {
Map<String, List<TetrioPlayer>> _players = {};
final Map<String, TetrioPlayer> _playersCache = {};
final Map<String, Map<String, dynamic>> _recordsCache = {};
final Map<String, dynamic> _replaysCache = {};
final Map<String, TetrioPlayersLeaderboard> _leaderboardsCache = {};
final Map<String, List<News>> _newsCache = {};
final Map<String, Map<String, double?>> _topTRcache = {};
@ -121,6 +122,12 @@ class TetrioService extends DB {
}
Future<List<dynamic>> szyGetReplay(String replayID) async {
try{
var cached = _replaysCache.entries.firstWhere((element) => element.key == replayID);
return cached.value;
}catch (e){
// actually going to obtain
}
Uri url = Uri.https('inoue.szy.lol', '/api/replay/$replayID');
var downloadPath = await getDownloadsDirectory();
downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory();
@ -132,6 +139,7 @@ class TetrioService extends DB {
switch (response.statusCode) {
case 200:
developer.log("szyDownload: Replay downloaded", name: "services/tetrio_crud", error: response.statusCode);
_replaysCache[replayID] = [response.body, response.bodyBytes];
return [response.body, response.bodyBytes];
case 404:
throw SzyNotFound();
@ -157,7 +165,6 @@ class TetrioService extends DB {
}
Future<String> SaveReplay(String replayID) async {
Uri url = Uri.https('inoue.szy.lol', '/api/replay/$replayID');
var downloadPath = await getDownloadsDirectory();
downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory();
var replayFile = File("${downloadPath.path}/$replayID.ttrm");

View File

@ -801,6 +801,7 @@ class CompareThingy extends StatelessWidget {
final bool higherIsBetter;
final int? fractionDigits;
final String? postfix;
final String? prefix;
const CompareThingy(
{super.key,
required this.greenSide,
@ -808,6 +809,7 @@ class CompareThingy extends StatelessWidget {
required this.label,
required this.higherIsBetter,
this.fractionDigits,
this.prefix,
this.postfix});
String verdict(num greenSide, num redSide, int fraction) {
@ -845,7 +847,7 @@ class CompareThingy extends StatelessWidget {
],
)),
child: Text(
f.format(greenSide) + (postfix ?? ""),
(prefix ?? "") + f.format(greenSide) + (postfix ?? ""),
style: const TextStyle(
fontSize: 22,
shadows: <Shadow>[
@ -899,7 +901,7 @@ class CompareThingy extends StatelessWidget {
],
)),
child: Text(
f.format(redSide) + (postfix ?? ""),
(prefix ?? "") + f.format(redSide) + (postfix ?? ""),
style: const TextStyle(
fontSize: 22,
shadows: <Shadow>[

View File

@ -269,10 +269,6 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
value: "history",
child: Text(t.fetchAndsaveTLHistory),
),
PopupMenuItem(
value: "test",
child: Text("Test replay reading"),
),
PopupMenuItem(
value: "/states",
child: Text(t.showStoredData),
@ -294,11 +290,6 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
case "history":
changePlayer(_searchFor, fetchHistory: true);
break;
case "test":
var replayfile = File("/home/dan63047/Загрузки/659337dd1eef65e513c5dc8d.ttrm").readAsStringSync();
var testObject = ReplayData.fromJson(jsonDecode(replayfile));
print("lol");
break;
default:
context.go(value);
}

View File

@ -201,13 +201,18 @@ class TlMatchResultState extends State<TlMatchResultView> {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
return CircularProgressIndicator();
return const LinearProgressIndicator();
case ConnectionState.done:
if (!snapshot.hasError){
var time = framesToTime(snapshot.data!.totalLength);
return Center(child: Text("Match Length: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}"));
if (roundSelector.isNegative){
var time = framesToTime(snapshot.data!.totalLength);
return Center(child: Text("Match Length: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}", textAlign: TextAlign.center));
}else{
var time = framesToTime(snapshot.data!.roundLengths[roundSelector]);
return Center(child: Text("Round Length: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}\nWinner: ${snapshot.data!.roundWinners[roundSelector][1]}", textAlign: TextAlign.center,));
}
}else{
return Text("skill issue");
return const Text("skill issue", textAlign: TextAlign.center);
}
}
@ -247,27 +252,47 @@ class TlMatchResultState extends State<TlMatchResultView> {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
return LinearProgressIndicator();
return const LinearProgressIndicator();
case ConnectionState.done:
if (!snapshot.hasError){
var greenSidePlayer = snapshot.data!.endcontext.indexWhere(((element) => element.userId == widget.initPlayerId));
var redSidePlayer = snapshot.data!.endcontext.indexWhere(((element) => element.userId != widget.initPlayerId));
var greenSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId == widget.initPlayerId);
var redSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId != widget.initPlayerId);
return Column(children: [
CompareThingy(greenSide: snapshot.data!.totalStats[greenSidePlayer].piecesPlaced,
redSide: snapshot.data!.totalStats[redSidePlayer].piecesPlaced,
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][greenSidePlayer].piecesPlaced,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][redSidePlayer].piecesPlaced,
label: "Pieces Placed", higherIsBetter: true),
CompareThingy(greenSide: snapshot.data!.totalStats[greenSidePlayer].linesCleared,
redSide: snapshot.data!.totalStats[redSidePlayer].linesCleared,
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][greenSidePlayer].linesCleared,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][redSidePlayer].linesCleared,
label: "Lines Cleared", higherIsBetter: true),
CompareThingy(greenSide: snapshot.data!.totalStats[greenSidePlayer].finessePercentage * 100,
redSide: snapshot.data!.totalStats[redSidePlayer].finessePercentage * 100,
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][greenSidePlayer].finessePercentage * 100,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][redSidePlayer].finessePercentage * 100,
label: "Finnese", postfix: "%", fractionDigits: 2, higherIsBetter: true),
CompareThingy(greenSide: snapshot.data!.totalStats[greenSidePlayer].topCombo,
redSide: snapshot.data!.totalStats[redSidePlayer].topCombo,
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topSpike : snapshot.data!.stats[roundSelector][greenSidePlayer].topSpike,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topSpike : snapshot.data!.stats[roundSelector][redSidePlayer].topSpike,
label: "Best Spike", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topCombo : snapshot.data!.stats[roundSelector][greenSidePlayer].topCombo,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topCombo : snapshot.data!.stats[roundSelector][redSidePlayer].topCombo,
label: "Best Combo", higherIsBetter: true),
CompareThingy(greenSide: snapshot.data!.totalStats[greenSidePlayer].topBtB,
redSide: snapshot.data!.totalStats[redSidePlayer].topBtB,
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topBtB : snapshot.data!.stats[roundSelector][greenSidePlayer].topBtB,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topBtB : snapshot.data!.stats[roundSelector][redSidePlayer].topBtB,
label: "Best BtB", higherIsBetter: true),
const Divider(),
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text("Garbage", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.sent,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.sent,
label: "Sent", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.recived,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.recived,
label: "Recived", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.attack,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.attack,
label: "Attack", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.cleared,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.cleared,
label: "Cleared", higherIsBetter: true),
],);
}else{
return Text("skill issue");
@ -412,17 +437,17 @@ class TlMatchResultState extends State<TlMatchResultView> {
CompareThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.das,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.das,
label: "DAS", fractionDigits: 1,
label: "DAS", fractionDigits: 1, postfix: "F",
higherIsBetter: false),
CompareThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.arr,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.arr,
label: "ARR", fractionDigits: 1,
label: "ARR", fractionDigits: 1, postfix: "F",
higherIsBetter: false),
CompareThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.sdf,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.sdf,
label: "SDF",
label: "SDF", prefix: "x",
higherIsBetter: true),
CompareBoolThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock,

View File

@ -166,6 +166,7 @@ flutter:
- res/tetrio_badges/sfu_raccoon_1.png
- res/tetrio_badges/sfu_raccoon_2.png
- res/tetrio_badges/sfu_raccoon_3.png
- res/tetrio_badges/streamersuperlobby.png
- res/tetrio_badges/superlobby.png
- res/tetrio_badges/superlobby2.png
- res/tetrio_badges/taws_u50_1.png
@ -185,6 +186,10 @@ flutter:
- res/tetrio_badges/ttsdtc_1.png
- res/tetrio_badges/ttsdtc_2.png
- res/tetrio_badges/ttsdtc_3.png
- res/tetrio_badges/twc23_1.png
- res/tetrio_badges/twc23_2.png
- res/tetrio_badges/twc23_3.png
- res/tetrio_badges/twc23_4.png
- res/tetrio_badges/ubcea_1.png
- res/tetrio_badges/ubcea_2.png
- res/tetrio_badges/ubcea_3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB