diff --git a/analysis_options.yaml b/analysis_options.yaml index 09918f1..0de7d1c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -3,6 +3,7 @@ analyzer: errors: library_private_types_in_public_api: ignore prefer_const_constructors: ignore + prefer_final_fields: ignore use_build_context_synchronously: ignore use_key_in_widget_constructors: ignore use_super_parameters: ignore diff --git a/lib/globals.dart b/lib/globals.dart index 4884e50..2e91c39 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -1,3 +1,3 @@ // lib/globals.dart -const String apiurl = "https://api.dthpp.mercurio.moe"; -//const String apiurl = "http://192.168.1.120:9134"; +//const String apiurl = "https://api.dthpp.mercurio.moe"; +const String apiurl = "http://192.168.1.120:9134"; diff --git a/lib/pages/views/creatematch.dart b/lib/pages/views/creatematch.dart index cd4f3e5..9a5bb37 100644 --- a/lib/pages/views/creatematch.dart +++ b/lib/pages/views/creatematch.dart @@ -12,9 +12,10 @@ class CreateMatchPage extends StatefulWidget { class _CreateMatchPageState extends State { String? _matchId; bool _isLoading = false; - bool _isTwoPlayerModeEnabled = false; // Track the toggle state + bool _isTwoPlayerModeEnabled = false; final String _createMatchApiUrl = '$apiurl/creatematch'; + final String _createDoubleMatchUrl = '$apiurl/creatematch_2v2'; Future _createMatch() async { setState(() { @@ -33,8 +34,10 @@ class _CreateMatchPageState extends State { } try { + final String apiUrl = + _isTwoPlayerModeEnabled ? _createDoubleMatchUrl : _createMatchApiUrl; final response = await http.post( - Uri.parse(_createMatchApiUrl), + Uri.parse(apiUrl), headers: {'Content-Type': 'application/json'}, body: json.encode({'token': token}), ); @@ -42,7 +45,10 @@ class _CreateMatchPageState extends State { if (response.statusCode == 200) { final data = json.decode(response.body); setState(() { - _matchId = data['match_id'].toString(); + _matchId = _isTwoPlayerModeEnabled + ? data['match_id'].toString() + + 'D' // Append "D" for two-player mode + : data['match_id'].toString(); }); _showToast('Match created successfully!'); } else { @@ -57,7 +63,6 @@ class _CreateMatchPageState extends State { } } - // Show a Toast message (SnackBar in Flutter) void _showToast(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message)), @@ -106,10 +111,6 @@ class _CreateMatchPageState extends State { setState(() { _isTwoPlayerModeEnabled = value; }); - if (_isTwoPlayerModeEnabled) { - _showToast( - 'We\'re sorry, this feature isn\'t available yet'); - } }, ), SizedBox(height: 16), diff --git a/lib/pages/views/joinmatch.dart b/lib/pages/views/joinmatch.dart index e823d46..b147657 100644 --- a/lib/pages/views/joinmatch.dart +++ b/lib/pages/views/joinmatch.dart @@ -13,14 +13,22 @@ class _JoinMatchPageState extends State { TextEditingController _matchIdController = TextEditingController(); bool _isJoined = false; bool _isLoading = false; + bool _is2v2Mode = false; + int _selectedSlot = 2; int _player1Score = 0; int _player2Score = 0; + int _player3Score = 0; + int _player4Score = 0; + String? _matchId; String? _player1name; String? _player2name; - String? _matchId; + List _players = []; + bool _canEdit = false; final String _joinMatchApiUrl = '$apiurl/joinmatch'; + final String _joinMatch2v2ApiUrl = '$apiurl/joinmatch_2v2'; final String _endMatchApiUrl = '$apiurl/endmatch'; + final String _endFourApiUrl = '$apiurl/endfour'; Future _joinMatch() async { setState(() { @@ -40,25 +48,60 @@ class _JoinMatchPageState extends State { } try { - final response = await http.post( - Uri.parse(_joinMatchApiUrl), - headers: {'Content-Type': 'application/json'}, - body: json.encode({'token': token, 'match_id': int.parse(matchId)}), - ); + if (_is2v2Mode) { + // Joining a 2v2 match + final response = await http.post( + Uri.parse(_joinMatch2v2ApiUrl), + headers: {'Content-Type': 'application/json'}, + body: json.encode({ + 'token': token, + 'match_id': int.parse(matchId), + 'slot': _selectedSlot, + }), + ); - if (response.statusCode == 200) { - final responseData = json.decode(response.body); - setState(() { - _isJoined = true; - _player1name = responseData['player1_name']; - _player2name = responseData['player2_name']; - _player1Score = 0; - _player2Score = 0; - _matchId = matchId; - }); - _showToast('Joined match successfully!'); + if (response.statusCode == 200) { + final responseData = json.decode(response.body); + setState(() { + _isJoined = true; + _matchId = matchId; + _players = List.from( + responseData['players'].map((p) => p['name'])); + _canEdit = responseData['canEdit']; + _player1Score = 0; + _player2Score = 0; + _player3Score = 0; + _player4Score = 0; + }); + _showToast('Joined match successfully!'); + } else { + _showToast('Failed to join 2v2 match.'); + } } else { - _showToast('Failed to join match.'); + // Joining a 1v1 match + final response = await http.post( + Uri.parse(_joinMatchApiUrl), + headers: {'Content-Type': 'application/json'}, + body: json.encode({ + 'token': token, + 'match_id': int.parse(matchId), + }), + ); + + if (response.statusCode == 200) { + final responseData = json.decode(response.body); + setState(() { + _isJoined = true; + _player1name = responseData['player1_name']; + _player2name = responseData['player2_name']; + _player1Score = 0; + _player2Score = 0; + _matchId = matchId; + }); + _showToast('Joined match successfully!'); + } else { + _showToast('Failed to join match.'); + } } } catch (e) { _showToast('Error: $e'); @@ -69,16 +112,6 @@ class _JoinMatchPageState extends State { } } - void _updateScore(int player, int delta) { - setState(() { - if (player == 1) { - _player1Score += delta; - } else if (player == 2) { - _player2Score += delta; - } - }); - } - Future _endMatch() async { setState(() { _isLoading = true; @@ -96,20 +129,42 @@ class _JoinMatchPageState extends State { } try { - final response = await http.post( - Uri.parse(_endMatchApiUrl), - headers: {'Content-Type': 'application/json'}, - body: json.encode({ - 'match_id': int.parse(_matchId!), - 'player1_score': _player1Score, - 'player2_score': _player2Score, - }), - ); + if (_is2v2Mode) { + // End the 2v2 match using the /endfour API + final response = await http.post( + Uri.parse(_endFourApiUrl), + headers: {'Content-Type': 'application/json'}, + body: json.encode({ + 'match_id': int.parse(_matchId!), + 'player1_team1_score': _player1Score, + 'player2_team1_score': _player2Score, + 'player1_team2_score': _player3Score, + 'player2_team2_score': _player4Score, + }), + ); - if (response.statusCode == 200) { - _showToast('Match ended successfully!'); + if (response.statusCode == 200) { + _showToast('2v2 match ended successfully!'); + } else { + _showToast('Failed to end 2v2 match.'); + } } else { - _showToast('Failed to end match.'); + // End the 1v1 match + final response = await http.post( + Uri.parse(_endMatchApiUrl), + headers: {'Content-Type': 'application/json'}, + body: json.encode({ + 'match_id': int.parse(_matchId!), + 'player1_score': _player1Score, + 'player2_score': _player2Score, + }), + ); + + if (response.statusCode == 200) { + _showToast('Match ended successfully!'); + } else { + _showToast('Failed to end match.'); + } } } catch (e) { _showToast('Error: $e'); @@ -138,96 +193,228 @@ class _JoinMatchPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - // Player 1 - Text( - _player1name ?? 'Player 1', - style: TextStyle( - fontSize: 18, - color: Colors.white, - ), - ), - SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.remove, color: Colors.white), - onPressed: () => _updateScore(1, -1), - ), - Container( - width: 100, - height: 50, - decoration: BoxDecoration( - border: - Border.all(color: Colors.white, width: 2), - borderRadius: BorderRadius.circular(8), - ), - child: Center( - child: Text( - '$_player1Score', - style: TextStyle( - fontSize: 24, - color: Colors.white, - fontWeight: FontWeight.bold, + // Display for 2v2 match + if (_is2v2Mode && _canEdit) ...[ + Text('2v2 Match', + style: + TextStyle(color: Colors.white, fontSize: 24)), + SizedBox(height: 16), + // First Team + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + Text(_players[0], + style: TextStyle(color: Colors.white)), + Row( + children: [ + IconButton( + icon: Icon(Icons.remove, + color: Colors.white), + onPressed: () => _updateScore(1, -1)), + Container( + width: 100, + height: 50, + decoration: BoxDecoration( + border: Border.all( + color: Colors.white, + width: 2), + borderRadius: + BorderRadius.circular(8)), + child: Center( + child: Text('$_player1Score', + style: TextStyle( + color: Colors.white, + fontWeight: + FontWeight.bold, + fontSize: 24)))), + IconButton( + icon: Icon(Icons.add, + color: Colors.white), + onPressed: () => _updateScore(1, 1)), + ], ), - ), + ], ), - ), - IconButton( - icon: Icon(Icons.add, color: Colors.white), - onPressed: () => _updateScore(1, 1), - ), - ], - ), - SizedBox(height: 8), - Divider( - color: Colors.white, - thickness: 2, - indent: 80, - endIndent: 80, - ), - SizedBox(height: 8), - // Player 2 - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.remove, color: Colors.white), - onPressed: () => _updateScore(2, -1), - ), - Container( - width: 100, - height: 50, - decoration: BoxDecoration( - border: - Border.all(color: Colors.white, width: 2), - borderRadius: BorderRadius.circular(8), - ), - child: Center( - child: Text( - '$_player2Score', - style: TextStyle( - fontSize: 24, - color: Colors.white, - fontWeight: FontWeight.bold, + SizedBox(width: 32), + Column( + children: [ + Text(_players[1], + style: TextStyle(color: Colors.white)), + Row( + children: [ + IconButton( + icon: Icon(Icons.remove, + color: Colors.white), + onPressed: () => _updateScore(2, -1)), + Container( + width: 100, + height: 50, + decoration: BoxDecoration( + border: Border.all( + color: Colors.white, + width: 2), + borderRadius: + BorderRadius.circular(8)), + child: Center( + child: Text('$_player2Score', + style: TextStyle( + color: Colors.white, + fontWeight: + FontWeight.bold, + fontSize: 24)))), + IconButton( + icon: Icon(Icons.add, + color: Colors.white), + onPressed: () => _updateScore(2, 1)), + ], ), - ), + ], ), - ), - IconButton( - icon: Icon(Icons.add, color: Colors.white), - onPressed: () => _updateScore(2, 1), - ), - ], - ), - SizedBox(height: 8), - Text( - _player2name ?? 'Player 2', - style: TextStyle( - fontSize: 18, - color: Colors.white, + ], ), - ), + SizedBox(height: 32), + // Second Team + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + Text(_players[2], + style: TextStyle(color: Colors.white)), + Row( + children: [ + IconButton( + icon: Icon(Icons.remove, + color: Colors.white), + onPressed: () => _updateScore(3, -1)), + Container( + width: 100, + height: 50, + decoration: BoxDecoration( + border: Border.all( + color: Colors.white, + width: 2), + borderRadius: + BorderRadius.circular(8)), + child: Center( + child: Text('$_player3Score', + style: TextStyle( + color: Colors.white, + fontWeight: + FontWeight.bold, + fontSize: 24)))), + IconButton( + icon: Icon(Icons.add, + color: Colors.white), + onPressed: () => _updateScore(3, 1)), + ], + ), + ], + ), + SizedBox(width: 32), + Column( + children: [ + Text(_players[3], + style: TextStyle(color: Colors.white)), + Row( + children: [ + IconButton( + icon: Icon(Icons.remove, + color: Colors.white), + onPressed: () => _updateScore(4, -1)), + Container( + width: 100, + height: 50, + decoration: BoxDecoration( + border: Border.all( + color: Colors.white, + width: 2), + borderRadius: + BorderRadius.circular(8)), + child: Center( + child: Text('$_player4Score', + style: TextStyle( + color: Colors.white, + fontWeight: + FontWeight.bold, + fontSize: 24)))), + IconButton( + icon: Icon(Icons.add, + color: Colors.white), + onPressed: () => _updateScore(4, 1)), + ], + ), + ], + ), + ], + ), + ], + // 1v1 Match UI + if (!_is2v2Mode) ...[ + Text(_player1name ?? 'Player 1', + style: TextStyle(color: Colors.white)), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon(Icons.remove, color: Colors.white), + onPressed: () => _updateScore(1, -1)), + Container( + width: 100, + height: 50, + decoration: BoxDecoration( + border: Border.all( + color: Colors.white, width: 2), + borderRadius: BorderRadius.circular(8)), + child: Center( + child: Text('$_player1Score', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 24)))), + IconButton( + icon: Icon(Icons.add, color: Colors.white), + onPressed: () => _updateScore(1, 1)), + ], + ), + SizedBox(height: 8), + Divider( + color: Colors.white, + thickness: 2, + indent: 80, + endIndent: 80), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon(Icons.remove, color: Colors.white), + onPressed: () => _updateScore(2, -1)), + Container( + width: 100, + height: 50, + decoration: BoxDecoration( + border: Border.all( + color: Colors.white, width: 2), + borderRadius: BorderRadius.circular(8)), + child: Center( + child: Text('$_player2Score', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 24)))), + IconButton( + icon: Icon(Icons.add, color: Colors.white), + onPressed: () => _updateScore(2, 1)), + ], + ), + SizedBox(height: 8), + Text(_player2name ?? 'Player 2', + style: TextStyle(color: Colors.white)), + ], SizedBox(height: 32), ElevatedButton( onPressed: _endMatch, @@ -249,6 +436,34 @@ class _JoinMatchPageState extends State { ), ), SizedBox(height: 16), + // Toggle for 2v2 Mode + SwitchListTile( + title: Text('Enable 2v2 Mode'), + value: _is2v2Mode, + onChanged: (value) { + setState(() { + _is2v2Mode = value; + }); + }, + ), + // If 2v2 is selected, display slot selection + if (_is2v2Mode) ...[ + DropdownButton( + value: _selectedSlot, + items: [2, 3, 4].map((int value) { + return DropdownMenuItem( + value: value, + child: Text('Slot $value'), + ); + }).toList(), + onChanged: (int? newValue) { + setState(() { + _selectedSlot = newValue!; + }); + }, + ), + ], + SizedBox(height: 16), ElevatedButton( onPressed: () { if (_matchIdController.text.isNotEmpty) { @@ -264,4 +479,18 @@ class _JoinMatchPageState extends State { ), ); } + + void _updateScore(int playerIndex, int increment) { + setState(() { + if (playerIndex == 1) { + _player1Score += increment; + } else if (playerIndex == 2) { + _player2Score += increment; + } else if (playerIndex == 3) { + _player3Score += increment; + } else if (playerIndex == 4) { + _player4Score += increment; + } + }); + } } diff --git a/lib/pages/views/leaderboard.dart b/lib/pages/views/leaderboard.dart index 129e939..53f1d53 100644 --- a/lib/pages/views/leaderboard.dart +++ b/lib/pages/views/leaderboard.dart @@ -85,8 +85,9 @@ class _LeaderboardPageState extends State { child: Text(player['player_name'][0].toUpperCase()), ), title: Text(player['player_name']), - subtitle: Text('Elo Rating: ${player['elo_rating']}'), - trailing: Text('Friend Code: ${player['friend_code']}'), + subtitle: Text( + 'Elo: ${player['elo_rating']} | TSC: ${player['osk_mu']}'), + trailing: Text('UID: ${player['friend_code']}'), ), ); }, diff --git a/lib/pages/views/myprofile.dart b/lib/pages/views/myprofile.dart index 70614eb..c2b4922 100644 --- a/lib/pages/views/myprofile.dart +++ b/lib/pages/views/myprofile.dart @@ -15,6 +15,8 @@ class _ProfilePageState extends State { String? _name; int? _uid; int? _elo; + double? _mu; + double? _unc; List _matches = []; final String _getProfileApiUrl = '$apiurl/getprofile'; @@ -47,6 +49,8 @@ class _ProfilePageState extends State { _name = data['name']; _uid = data['uid']; _elo = data['elo']; + _mu = data['osk_mu']; + _unc = data['osk_sig']; _matches = data['matches']; _isLoading = false; }); @@ -85,6 +89,75 @@ class _ProfilePageState extends State { return '$apiurl/assets/none.png'; } + void _showExplanationDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('ELO, OSK, and UNC Explanation'), + content: SingleChildScrollView( + child: ListBody( + children: [ + // ELO Section + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "ELO (Elo Rating):", + style: TextStyle(fontWeight: FontWeight.bold), + ), + SizedBox(height: 4.0), + Text( + "ELO is a widely-used rating system designed to measure the relative skill levels of players in two-player games. It was my initial pick for a testing environment since I only really thought about 1v1 matches, and because it had a readily available Python implementation. I'm lazy.", + ), + SizedBox(height: 8.0), + ], + ), + // OSK Section + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "OSK (OpenSkill Mu):", + style: TextStyle(fontWeight: FontWeight.bold), + ), + SizedBox(height: 4.0), + Text( + "OSKmu is a skill rating system based on the OpenSkill model, which is a probabilistic framework for estimating a player's skill. Unlike ELO, which is purely a point-based system, OpenSkill Mu takes into account not just the outcome of matches but also the degree of uncertainty in a player's skill estimation. Since I set up the system to have a 0-base-elo, I had to adapt the OpenSkill implementation to a standard 25 OSK and 8.33 uncertainty.", + ), + SizedBox(height: 8.0), + ], + ), + // UNC Section + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Uncertainty (UNC):", + style: TextStyle(fontWeight: FontWeight.bold), + ), + SizedBox(height: 4.0), + Text( + "This is a measure of how confident the system is about a player's skill rating. A higher uncertainty value means the system is less confident about the accuracy of the player's skill estimation, while a lower uncertainty indicates more confidence in the player's rating.", + ), + ], + ), + ], + ), + ), + actions: [ + TextButton( + child: Text('Close'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -122,9 +195,19 @@ class _ProfilePageState extends State { fontWeight: FontWeight.bold, ), ), - SizedBox(height: 8), + SizedBox(height: 6), Text('UID: ${_uid ?? 'N/A'}'), - Text('ELO: ${_elo ?? 'N/A'}'), + Row( + children: [ + Text( + 'ELO: ${_elo ?? 'N/A'} | OSK: ${_mu != null ? _mu?.toStringAsFixed(3) : 'N/A'} | UNC: ${_unc != null ? _unc?.toStringAsFixed(3) : 'N/A'}'), + IconButton( + icon: Icon(Icons.help_outline), + onPressed: _showExplanationDialog, + tooltip: 'What are ELO, OSK, and UNC?', + ), + ], + ), ], ), SizedBox(width: 25),