Skip to content

Commit

Permalink
Support displaying the advantage graph
Browse files Browse the repository at this point in the history
  • Loading branch information
calcitem committed Dec 8, 2024
1 parent 0b61f70 commit d014c33
Show file tree
Hide file tree
Showing 65 changed files with 365 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class DisplaySettings {
this.customWhitePieceImagePath,
this.customBlackPieceImagePath,
this.boardCornerRadius = 5.0,
this.isAdvantageGraphShown = false,
});

/// Encodes a Json style map into a [DisplaySettings] object
Expand Down Expand Up @@ -206,6 +207,9 @@ class DisplaySettings {
@HiveField(35, defaultValue: 5.0)
final double boardCornerRadius;

@HiveField(36, defaultValue: false)
final bool isAdvantageGraphShown;

/// Decodes a Json from a [DisplaySettings] object
Map<String, dynamic> toJson() => _$DisplaySettingsToJson(this);
}
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,12 @@ class AppearanceSettingsPage extends StatelessWidget {
.copyWith(isPositionalAdvantageIndicatorShown: val),
titleString: S.of(context).showPositionalAdvantageIndicator,
),
SettingsListTile.switchTile(
value: displaySettings.isAdvantageGraphShown,
onChanged: (bool val) => DB().displaySettings =
displaySettings.copyWith(isAdvantageGraphShown: val),
titleString: S.of(context).showAdvantageGraph,
),
SettingsListTile(
titleString: S.of(context).boardCornerRadius,
onTap: () => setBoardCornerRadius(context),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// This file is part of Sanmill.
// Copyright (C) 2019-2024 The Sanmill developers (see AUTHORS file)
//
// Sanmill is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Sanmill is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

import 'dart:math' as math;

import 'package:flutter/material.dart';

import '../../../shared/database/database.dart';

/// A custom painter to draw the advantage trend line, but only for the most recent 20 moves.
/// Positive values indicate white advantage, negative indicate black advantage.
/// If there are more than 20 moves, only the last 20 are shown.
/// As new moves come in, older moves are discarded from the left side.
class AdvantageGraphPainter extends CustomPainter {
AdvantageGraphPainter(this.data);

final List<int> data;

@override
void paint(Canvas canvas, Size size) {
// Determine how many data points to show (up to 20).
final int showCount = math.min(20, data.length);

// If not enough data points, do nothing.
if (showCount < 2) {
return;
}

// Extract the subset of the data (last showCount moves).
final List<int> shownData = data.sublist(data.length - showCount);

// Paint for the advantage line.
final Paint linePaint = Paint()
..color = Color.lerp(
DB().colorSettings.whitePieceColor,
DB().colorSettings.blackPieceColor,
0.5,
)!
..strokeWidth = 2
..style = PaintingStyle.stroke;

// Paint for the zero advantage line.
final Paint zeroLinePaint = Paint()
..color = DB().colorSettings.boardBackgroundColor
..strokeWidth = 1
..style = PaintingStyle.stroke;

const double margin = 10.0;
final double chartWidth = size.width - margin * 2;
final double chartHeight = size.height - margin * 2;

// Zero line (value=0) in the vertical center.
final double zeroY = margin + chartHeight / 2;

// Function to map advantage values [-100, 100] to canvas Y coordinates.
double valueToPixel(int val) {
// val=100 => top (margin)
// val=-100 => bottom (margin+chartHeight)
// val=0 => zeroY
return zeroY - (val * (chartHeight / 200.0));
}

// Horizontal step between points for the shownData subset.
final int count = shownData.length;
final double dxStep = chartWidth / (count - 1);

final Path path = Path();
for (int i = 0; i < count; i++) {
final double x = margin + i * dxStep;
final double y = valueToPixel(shownData[i]);
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}

// Draw the zero advantage line.
canvas.drawLine(
Offset(margin, zeroY),
Offset(size.width - margin, zeroY),
zeroLinePaint,
);

// Draw the line path.
canvas.drawPath(path, linePaint);
}

@override
bool shouldRepaint(AdvantageGraphPainter oldDelegate) {
return oldDelegate.data != data;
}
}
80 changes: 61 additions & 19 deletions src/ui/flutter_app/lib/game_page/widgets/play_area.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ import '../../shared/database/database.dart';
import '../../shared/services/screenshot_service.dart';
import '../../shared/themes/app_theme.dart';
import '../services/mill.dart';
import '../services/painters/advantage_graph_painter.dart';
import 'game_page.dart';
import 'modals/game_options_modal.dart';
import 'toolbars/game_toolbar.dart';

/// The PlayArea widget is the main content of the game page.
class PlayArea extends StatefulWidget {
/// Creates a PlayArea widget.
///
Expand All @@ -43,23 +45,41 @@ class PlayArea extends StatefulWidget {
}

class PlayAreaState extends State<PlayArea> {
/// A list to store historical advantage values.
List<int> advantageData = <int>[];

@override
void initState() {
super.initState();
// Listen to changes in header icons to update the UI accordingly.
// Listen to changes in header icons (usually triggered after a move).
GameController().headerIconsNotifier.addListener(_updateUI);

// Optionally, initialize advantageData with the current value if needed:
advantageData.add(_getCurrentAdvantageValue());
}

@override
void dispose() {
// Remove the listener when disposing to prevent memory leaks.
GameController().headerIconsNotifier.removeListener(_updateUI);
super.dispose();
}

/// Retrieve the current advantage value from GameController.
/// value > 0 means white advantage, value < 0 means black advantage.
/// The range is [-100, 100].
int _getCurrentAdvantageValue() {
final int value =
GameController().value == null ? 0 : int.parse(GameController().value!);
return value;
}

/// Updates the UI by calling setState.
/// Also append the current advantage value to the list so that
/// after each move, the chart will reflect the latest advantage trend.
void _updateUI() {
setState(() {});
setState(() {
advantageData.add(_getCurrentAdvantageValue());
});
}

/// Takes a screenshot and saves it to the specified [storageLocation] with an optional [filename].
Expand Down Expand Up @@ -343,7 +363,7 @@ class PlayAreaState extends State<PlayArea> {
// Check if the toolbar should be displayed at the bottom.
final bool isToolbarAtBottom = DB().displaySettings.isToolbarAtBottom;

// Build the main content of the page.
// The main column of the page content (without the bottom toolbars).
final Widget mainContent = SizedBox(
width: dimension,
child: SafeArea(
Expand All @@ -356,7 +376,7 @@ class PlayAreaState extends State<PlayArea> {
child: Column(
children: <Widget>[
GameHeader(),
// Conditionally display the piece count row based on display settings and game mode.
// Display piece counts if needed
if ((DB().displaySettings.isUnplacedAndRemovedPiecesShown ||
GameController().gameInstance.gameMode ==
GameMode.setupPosition) &&
Expand All @@ -365,18 +385,19 @@ class PlayAreaState extends State<PlayArea> {
_buildPieceCountRow()
else
const SizedBox(height: AppTheme.boardMargin),
// Display the game board with the selected board image.

// The game board
NativeScreenshot(
controller: ScreenshotService.screenshotController,
child: Container(
alignment: Alignment.center,
child: GameBoard(
boardImage: widget
.boardImage, // Pass the ImageProvider to GameBoard
boardImage: widget.boardImage,
),
),
),
// Conditionally display the removed piece count row.

// Display removed piece count if needed
if ((DB().displaySettings.isUnplacedAndRemovedPiecesShown ||
GameController().gameInstance.gameMode ==
GameMode.setupPosition) &&
Expand All @@ -385,12 +406,26 @@ class PlayAreaState extends State<PlayArea> {
_buildRemovedPieceCountRow()
else
const SizedBox(height: AppTheme.boardMargin),
// Display the setup position toolbar if in setup mode and toolbar is not at bottom.

// Insert the advantage trend chart below the board and above the next toolbar rows.
// Only show if there's data.
if (DB().displaySettings.isAdvantageGraphShown &&
advantageData.isNotEmpty)
SizedBox(
height: 150,
width: double.infinity,
child: CustomPaint(
painter: AdvantageGraphPainter(advantageData),
),
),

// Setup position toolbar if in setup mode and toolbar is not at bottom
if (GameController().gameInstance.gameMode ==
GameMode.setupPosition &&
!isToolbarAtBottom)
const SetupPositionToolbar(),
// Display the history navigation toolbar based on display settings and game mode.

// History navigation toolbar if enabled, not in setup mode, and not at bottom
if (DB().displaySettings.isHistoryNavigationToolbarShown &&
GameController().gameInstance.gameMode !=
GameMode.setupPosition &&
Expand All @@ -402,7 +437,8 @@ class PlayAreaState extends State<PlayArea> {
children: _buildToolbarItems(
context, _getHistoryNavToolbarItems(context)),
),
// Display the analysis toolbar if enabled in display settings and toolbar is not at bottom.

// Analysis toolbar if enabled and not at bottom
if (DB().displaySettings.isAnalysisToolbarShown &&
!isToolbarAtBottom)
GamePageToolbar(
Expand All @@ -412,7 +448,8 @@ class PlayAreaState extends State<PlayArea> {
children: _buildToolbarItems(
context, _getAnalysisToolbarItems(context)),
),
// Display the main toolbar if not in setup mode and toolbar is not at bottom.

// Main toolbar if not in setup mode and not at bottom
if (GameController().gameInstance.gameMode !=
GameMode.setupPosition &&
!isToolbarAtBottom)
Expand All @@ -423,14 +460,15 @@ class PlayAreaState extends State<PlayArea> {
children: _buildToolbarItems(
context, _getMainToolbarItems(context)),
),

const SizedBox(height: AppTheme.boardMargin),
],
),
),
),
);

// If toolbar should be at the bottom, use a Column with main content and toolbar separated.
// If toolbar should be at the bottom, separate them.
if (isToolbarAtBottom) {
return SizedBox(
width: dimension,
Expand All @@ -440,12 +478,14 @@ class PlayAreaState extends State<PlayArea> {
left: false,
child: Column(
children: <Widget>[
// Expanded to take up available space above the toolbar
Expanded(child: mainContent),
// Display all toolbars at the bottom

// Setup position toolbar if in setup mode
if (GameController().gameInstance.gameMode ==
GameMode.setupPosition)
const SetupPositionToolbar(),

// History navigation toolbar if enabled and not in setup mode
if (DB().displaySettings.isHistoryNavigationToolbarShown &&
GameController().gameInstance.gameMode !=
GameMode.setupPosition)
Expand All @@ -456,7 +496,8 @@ class PlayAreaState extends State<PlayArea> {
children: _buildToolbarItems(
context, _getHistoryNavToolbarItems(context)),
),
// Display the analysis toolbar if enabled in display settings.

// Analysis toolbar if enabled
if (DB().displaySettings.isAnalysisToolbarShown)
GamePageToolbar(
backgroundColor:
Expand All @@ -465,7 +506,8 @@ class PlayAreaState extends State<PlayArea> {
children: _buildToolbarItems(
context, _getAnalysisToolbarItems(context)),
),
// Display the main toolbar if not in setup mode.

// Main toolbar if not in setup mode
if (GameController().gameInstance.gameMode !=
GameMode.setupPosition)
GamePageToolbar(
Expand All @@ -482,7 +524,7 @@ class PlayAreaState extends State<PlayArea> {
);
}

// If toolbar is not at the bottom, return the main content as is.
// If toolbar is not at the bottom, just return the main content.
return mainContent;
},
);
Expand Down
4 changes: 3 additions & 1 deletion src/ui/flutter_app/lib/l10n/intl_af.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1615,5 +1615,7 @@
"daSanQi": "Da San Qi",
"@daSanQi": {},
"useOpeningBook_Detail": "Laat die AI openingstrekke gebruik wat algemeen deur professionele menslike spelers tydens die vroeë spel gebruik word. Let wel: Hierdie funksie is tans slegs beskikbaar vir spesifieke reël stelle.",
"@useOpeningBook_Detail": {}
"@useOpeningBook_Detail": {},
"showAdvantageGraph": "Wys voordeelgrafiek",
"@showAdvantageGraph": {}
}
4 changes: 3 additions & 1 deletion src/ui/flutter_app/lib/l10n/intl_am.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1615,5 +1615,7 @@
"daSanQi": "ዳሳንቺ",
"@daSanQi": {},
"useOpeningBook_Detail": "በጀርባ ጨዋታ ላይ ባለሙያ ሰው ተጫዋቾች በሚጠቀሙበት ቀጥታ ተንቀሳቃሽ እንዲጠቀም AI ያስተውል። ማስታወሻ: ይህ ባህሪ አሁን ብቻ ለክነት ህጎች ስብስብ ተመድቦ ነው።",
"@useOpeningBook_Detail": {}
"@useOpeningBook_Detail": {},
"showAdvantageGraph": "የጥቅም ግራፍን አሳይ",
"@showAdvantageGraph": {}
}
4 changes: 3 additions & 1 deletion src/ui/flutter_app/lib/l10n/intl_ar.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1517,5 +1517,7 @@
"daSanQi": "دا سان تشي",
"@daSanQi": {},
"useOpeningBook_Detail": "دع الذكاء الاصطناعي يستخدم الافتتاحيات التي يستخدمها اللاعبون المحترفون في بداية اللعبة. ملاحظة: هذه الخاصية متاحة حاليًا لمجموعات قواعد محددة فقط.",
"@useOpeningBook_Detail": {}
"@useOpeningBook_Detail": {},
"showAdvantageGraph": "إظهار مخطط الميزة",
"@showAdvantageGraph": {}
}
4 changes: 3 additions & 1 deletion src/ui/flutter_app/lib/l10n/intl_az.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1615,5 +1615,7 @@
"daSanQi": "Da San Qi",
"@daSanQi": {},
"useOpeningBook_Detail": "AI-ya oyunun əvvəlində peşəkar insan oyunçular tərəfindən geniş istifadə edilən açılış hərəkətlərini istifadə etməyə icazə ver. Qeyd: Bu xüsusiyyət hazırda yalnız müəyyən qayda dəstləri üçün mövcuddur.",
"@useOpeningBook_Detail": {}
"@useOpeningBook_Detail": {},
"showAdvantageGraph": "Üstünlük qrafikini göstər",
"@showAdvantageGraph": {}
}
4 changes: 3 additions & 1 deletion src/ui/flutter_app/lib/l10n/intl_be.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1615,5 +1615,7 @@
"daSanQi": "Da San Qi",
"@daSanQi": {},
"useOpeningBook_Detail": "Дазвольце ШІ выкарыстоўваць стартавыя ходы, якія звычайна выкарыстоўваюцца прафесійнымі гульцамі на раннім этапе гульні. Заўвага: Гэтая функцыя на дадзены момант даступная толькі для канкрэтных набораў правілаў.",
"@useOpeningBook_Detail": {}
"@useOpeningBook_Detail": {},
"showAdvantageGraph": "Паказаць графік перавагі",
"@showAdvantageGraph": {}
}
Loading

0 comments on commit d014c33

Please sign in to comment.