feat: implement comprehensive statistics tracking and refactor checkout engine
BREAKING CHANGE: Database schema updated from v3 to v9 (destructive migration) Major Features: - Statistics system: Track player performance (darts, points, matches, double-outs) - Match state management: ONGOING/COMPLETED/CANCELED with state-based queries - Settings activity: Day/night mode and standard game mode preferences - CheckoutEngine refactored to standalone utility class with 1/2/3-dart methods CheckoutConstants Overhaul: - Generate all possible double-out combinations (~37,200 routes) - Intelligent route selection (fewer darts > T20/T19 > higher doubles) - Store both optimal routes and complete alternatives GameActivity Enhancements: - Automatic statistics tracking on turn submission and win - Career average calculation and database updates - Fixed race condition with dart value capture Database Changes: - Added Statistics entity and StatisticsDao - Player ID migration: int → long for consistency - Match entity: added MatchState enum and helper methods - MatchDao: new state-based query methods Developer Experience: - Comprehensive JavaDoc across all new/modified classes - Test harness for checkout generation validation - Improved code organization with utils package separation
This commit is contained in:
38
TestCheckoutGeneration.java
Normal file
38
TestCheckoutGeneration.java
Normal file
@@ -0,0 +1,38 @@
|
||||
import com.aldo.apps.ochecompanion.utils.CheckoutConstants;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Quick test to verify checkout generation statistics.
|
||||
*/
|
||||
public class TestCheckoutGeneration {
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== Checkout Generation Statistics ===");
|
||||
System.out.println("Total unique scores with checkouts: " + CheckoutConstants.getAvailableCheckouts().size());
|
||||
System.out.println("Total checkout combinations: " + CheckoutConstants.getTotalRoutesCount());
|
||||
|
||||
System.out.println("\n=== Sample Checkouts ===");
|
||||
|
||||
// Test some common scores
|
||||
int[] testScores = {2, 40, 50, 100, 120, 141, 170};
|
||||
for (int score : testScores) {
|
||||
String[] optimal = CheckoutConstants.getCheckoutRoute(score);
|
||||
List<String[]> all = CheckoutConstants.getAllCheckoutRoutes(score);
|
||||
|
||||
if (optimal != null) {
|
||||
System.out.println("\nScore " + score + ":");
|
||||
System.out.println(" Optimal: " + String.join(", ", optimal));
|
||||
System.out.println(" Total routes: " + (all != null ? all.size() : 0));
|
||||
|
||||
// Show first 3 alternatives if available
|
||||
if (all != null && all.size() > 1) {
|
||||
System.out.println(" Alternatives:");
|
||||
for (int i = 0; i < Math.min(3, all.size()); i++) {
|
||||
if (!java.util.Arrays.equals(all.get(i), optimal)) {
|
||||
System.out.println(" - " + String.join(", ", all.get(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,10 +38,12 @@ dependencies {
|
||||
implementation(libs.material)
|
||||
implementation(libs.activity)
|
||||
implementation(libs.constraintlayout)
|
||||
implementation(libs.preference)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.ext.junit)
|
||||
androidTestImplementation(libs.espresso.core)
|
||||
implementation(libs.glide)
|
||||
implementation(libs.room.runtime)
|
||||
annotationProcessor(libs.room.compiler)
|
||||
implementation(libs.preferences)
|
||||
}
|
||||
@@ -16,6 +16,10 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.OcheCompanion">
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_settings" />
|
||||
<activity
|
||||
android:name=".GameActivity"
|
||||
android:exported="false" />
|
||||
|
||||
@@ -17,8 +17,13 @@ import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.aldo.apps.ochecompanion.database.AppDatabase;
|
||||
import com.aldo.apps.ochecompanion.database.objects.Player;
|
||||
import com.aldo.apps.ochecompanion.database.objects.Statistics;
|
||||
import com.aldo.apps.ochecompanion.ui.CropOverlayView;
|
||||
import com.aldo.apps.ochecompanion.utils.UIConstants;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
@@ -110,7 +115,7 @@ public class AddPlayerActivity extends AppCompatActivity {
|
||||
/**
|
||||
* Database ID of the player being edited (-1 for new player).
|
||||
*/
|
||||
private int mExistingPlayerId = -1;
|
||||
private long mExistingPlayerId = -1;
|
||||
|
||||
/**
|
||||
* Player object loaded from the database (null when creating new player).
|
||||
@@ -162,6 +167,12 @@ public class AddPlayerActivity extends AppCompatActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_add_player);
|
||||
Log.d(TAG, "AddPlayerActivity Created");
|
||||
// Configure window insets to properly handle system bars (status bar, navigation bar)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
|
||||
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
|
||||
return insets;
|
||||
});
|
||||
|
||||
// Initialize all UI components and their click listeners
|
||||
initViews();
|
||||
@@ -171,7 +182,7 @@ public class AddPlayerActivity extends AppCompatActivity {
|
||||
|
||||
// Check if editing an existing player
|
||||
if (getIntent().hasExtra(EXTRA_PLAYER_ID)) {
|
||||
mExistingPlayerId = getIntent().getIntExtra(EXTRA_PLAYER_ID, -1);
|
||||
mExistingPlayerId = getIntent().getLongExtra(EXTRA_PLAYER_ID, -1);
|
||||
loadExistingPlayer();
|
||||
}
|
||||
}
|
||||
@@ -459,8 +470,14 @@ public class AddPlayerActivity extends AppCompatActivity {
|
||||
AppDatabase.getDatabase(this).playerDao().update(mExistingPlayer);
|
||||
} else {
|
||||
// Create and insert new player
|
||||
Player p = new Player(name, mInternalImagePath);
|
||||
AppDatabase.getDatabase(this).playerDao().insert(p);
|
||||
final Player p = new Player(name, mInternalImagePath);
|
||||
final long newUserId = AppDatabase.getDatabase(this).playerDao().insert(p);
|
||||
final Player dbPlayer = AppDatabase.getDatabase(this).playerDao().getPlayerById(newUserId);
|
||||
if (dbPlayer != null) {
|
||||
Log.d(TAG, "savePlayer: Player has been created, create stats as well.");
|
||||
final Statistics playerStats = new Statistics(dbPlayer.id);
|
||||
AppDatabase.getDatabase(this).statisticsDao().insertStatistics(playerStats);
|
||||
}
|
||||
}
|
||||
|
||||
// Close activity on main thread after save completes
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
@@ -14,8 +15,16 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.aldo.apps.ochecompanion.database.AppDatabase;
|
||||
import com.aldo.apps.ochecompanion.database.objects.Match;
|
||||
import com.aldo.apps.ochecompanion.database.objects.Player;
|
||||
import com.aldo.apps.ochecompanion.database.objects.Statistics;
|
||||
import com.aldo.apps.ochecompanion.utils.CheckoutConstants;
|
||||
import com.aldo.apps.ochecompanion.utils.CheckoutEngine;
|
||||
import com.aldo.apps.ochecompanion.utils.DartsConstants;
|
||||
import com.aldo.apps.ochecompanion.utils.UIConstants;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
@@ -30,6 +39,8 @@ import java.util.UUID;
|
||||
*/
|
||||
public class GameActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = "GameActivity";
|
||||
|
||||
/**
|
||||
* Intent extra key for player list. Type: ArrayList<Player>
|
||||
*/
|
||||
@@ -175,21 +186,33 @@ public class GameActivity extends AppCompatActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_game);
|
||||
|
||||
// Configure window insets to properly handle system bars (status bar, navigation bar)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
|
||||
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
|
||||
return insets;
|
||||
});
|
||||
|
||||
// Extract game parameters from intent
|
||||
mStartingScore = getIntent().getIntExtra(EXTRA_START_SCORE, DartsConstants.DEFAULT_GAME_SCORE);
|
||||
mMatchUuid = getIntent().getStringExtra(EXTRA_MATCH_UUID);
|
||||
ArrayList<Player> participants = getIntent().getParcelableArrayListExtra(EXTRA_PLAYERS);
|
||||
|
||||
// Initialize activity components in order
|
||||
initViews();
|
||||
setupKeyboard();
|
||||
setupGame(participants);
|
||||
new Thread(() -> {
|
||||
final List<Player> allAvailablePlayers = AppDatabase.getDatabase(GameActivity.this).playerDao().getAllPlayers();
|
||||
Log.d(TAG, "onCreate: allAvailablePlayers = [" + allAvailablePlayers + "]");
|
||||
runOnUiThread(() -> {
|
||||
setupGame(allAvailablePlayers);
|
||||
});
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes UI component references and sets up click listeners.
|
||||
*/
|
||||
private void initViews() {
|
||||
Log.d(TAG, "initViews() called");
|
||||
tvScorePrimary = findViewById(R.id.tvScorePrimary);
|
||||
tvPlayerName = findViewById(R.id.tvPlayerName);
|
||||
tvLegAvg = findViewById(R.id.tvLegAvg);
|
||||
@@ -218,6 +241,7 @@ public class GameActivity extends AppCompatActivity {
|
||||
* Dynamically creates and configures numeric keyboard buttons (1-20).
|
||||
*/
|
||||
private void setupKeyboard() {
|
||||
Log.d(TAG, "setupKeyboard() called");
|
||||
glKeyboard.removeAllViews();
|
||||
mKeyboardButtons.clear();
|
||||
|
||||
@@ -243,13 +267,15 @@ public class GameActivity extends AppCompatActivity {
|
||||
* @param players List of Player objects (can be null/empty)
|
||||
*/
|
||||
private void setupGame(final List<Player> players) {
|
||||
Log.d(TAG, "setupGame() called with: players = [" + players + "]");
|
||||
mPlayerStates = new ArrayList<>();
|
||||
if (players != null && !players.isEmpty()) {
|
||||
for (Player p : players) {
|
||||
mPlayerStates.add(new X01State(p.username, mStartingScore));
|
||||
mPlayerStates.add(new X01State(p, mStartingScore));
|
||||
}
|
||||
} else {
|
||||
mPlayerStates.add(new X01State("GUEST 1", mStartingScore));
|
||||
final Player guest = new Player("GUEST", null);
|
||||
mPlayerStates.add(new X01State(guest, mStartingScore));
|
||||
}
|
||||
updateUI();
|
||||
setMultiplier(DartsConstants.MULTIPLIER_SINGLE);
|
||||
@@ -289,7 +315,7 @@ public class GameActivity extends AppCompatActivity {
|
||||
mCurrentTurnDarts.add(points);
|
||||
updateTurnIndicators();
|
||||
mIsTurnOver = true;
|
||||
handleWin(active);
|
||||
handleWin(active, mCurrentTurnDarts.size(), scoreBeforeDart);
|
||||
} else {
|
||||
// VALID THROW
|
||||
mCurrentTurnDarts.add(points);
|
||||
@@ -350,6 +376,31 @@ public class GameActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePlayerStats(GameActivity.X01State active, int dartsThrown, int pointsMade, boolean wasBust) {
|
||||
new Thread(() -> {
|
||||
final Player player = active.player;
|
||||
if (player != null && player.id != 0) {
|
||||
final Statistics playerStats = AppDatabase.getDatabase(GameActivity.this).statisticsDao().getStatisticsForPlayer(player.id);
|
||||
playerStats.saveDartsThrown(dartsThrown, pointsMade);
|
||||
Log.d(TAG, "submitTurn: dartsThrown = [" + dartsThrown + "], pointsMade = [" + pointsMade + "]");
|
||||
if (!wasBust && dartsThrown < 3) {
|
||||
playerStats.addMissedDarts(3 - dartsThrown);
|
||||
}
|
||||
Log.d(TAG, "submitTurn: statistics = [" + playerStats + "]");
|
||||
AppDatabase.getDatabase(GameActivity.this).statisticsDao().updateStatistics(playerStats);
|
||||
|
||||
// Calculate career average: total points / total darts thrown
|
||||
final long totalDarts = playerStats.getDartsThrown();
|
||||
if (totalDarts > 0) {
|
||||
player.careerAverage = (double) playerStats.getOverallPointsMade() / totalDarts;
|
||||
} else {
|
||||
player.careerAverage = 0.0;
|
||||
}
|
||||
AppDatabase.getDatabase(GameActivity.this).playerDao().update(player);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes current turn and advances to next player.
|
||||
* Updates player score (unless bust), rotates to next player, resets turn state.
|
||||
@@ -377,7 +428,7 @@ public class GameActivity extends AppCompatActivity {
|
||||
active.remainingScore = finalScore;
|
||||
active.dartsThrown += mCurrentTurnDarts.size();
|
||||
}
|
||||
// If bust, score remains unchanged
|
||||
updatePlayerStats(active, mCurrentTurnDarts.size(), turnTotal, isBust);
|
||||
|
||||
// Rotate to next player
|
||||
mActivePlayerIndex = (mActivePlayerIndex + 1) % mPlayerStates.size();
|
||||
@@ -468,7 +519,7 @@ public class GameActivity extends AppCompatActivity {
|
||||
*/
|
||||
private void updateCheckoutSuggestion(final int score, final int dartsLeft) {
|
||||
if (score <= DartsConstants.MAX_CHECKOUT_SCORE && score > DartsConstants.BUST_SCORE && dartsLeft > 0) {
|
||||
String route = CheckoutEngine.getRoute(score, dartsLeft);
|
||||
String route = CheckoutEngine.calculateCheckout(score, dartsLeft);
|
||||
|
||||
if (route != null) {
|
||||
layoutCheckoutSuggestion.setVisibility(View.VISIBLE);
|
||||
@@ -524,7 +575,8 @@ public class GameActivity extends AppCompatActivity {
|
||||
*
|
||||
* @param winner X01State of the winning player
|
||||
*/
|
||||
private void handleWin(final X01State winner) {
|
||||
private void handleWin(final X01State winner, final int dartsThrown, final int pointsMade) {
|
||||
updatePlayerStats(winner, dartsThrown, pointsMade, false);
|
||||
// Show win notification
|
||||
Toast.makeText(this, winner.name + " WINS!", Toast.LENGTH_LONG).show();
|
||||
|
||||
@@ -543,10 +595,16 @@ public class GameActivity extends AppCompatActivity {
|
||||
* Tracks name, remaining score, and darts thrown.
|
||||
*/
|
||||
private static class X01State {
|
||||
|
||||
/**
|
||||
* Player's display name.
|
||||
* The {@link Player} object to update stats as well.
|
||||
*/
|
||||
String name;
|
||||
final Player player;
|
||||
|
||||
/**
|
||||
* Player's display name for convenience purpose, as extracted from the player object.
|
||||
*/
|
||||
final String name;
|
||||
|
||||
/**
|
||||
* Player's current remaining score.
|
||||
@@ -561,103 +619,13 @@ public class GameActivity extends AppCompatActivity {
|
||||
/**
|
||||
* Constructs X01State for a player.
|
||||
*
|
||||
* @param name Player's display name
|
||||
* @param player The actual {@link Player} instance.
|
||||
* @param startScore Starting score for the game
|
||||
*/
|
||||
X01State(final String name, final int startScore) {
|
||||
this.name = name;
|
||||
X01State(final Player player, final int startScore) {
|
||||
this.player = player;
|
||||
this.name = player.username;
|
||||
this.remainingScore = startScore;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper class that provides optimal checkout route suggestions for X01 games.
|
||||
* <p>
|
||||
* The CheckoutEngine calculates the best way to finish a game given a target score
|
||||
* and number of darts remaining. It combines pre-calculated routes for classic
|
||||
* finishes with intelligent logic for direct doubles and setup darts.
|
||||
* </p>
|
||||
* <p>
|
||||
* <strong>Checkout Logic Priority:</strong>
|
||||
* <ol>
|
||||
* <li><strong>Direct Doubles:</strong> For scores ≤40 and even, suggest immediate double</li>
|
||||
* <li><strong>Bullseye:</strong> For score of 50, suggest BULL (double bull)</li>
|
||||
* <li><strong>Setup Darts:</strong> For odd scores with multiple darts, suggest setup + double</li>
|
||||
* <li><strong>Pre-calculated Routes:</strong> For common finishes like 170, 141</li>
|
||||
* <li><strong>Fallback:</strong> Generic high-score route (T20 Route)</li>
|
||||
* </ol>
|
||||
* </p>
|
||||
* <p>
|
||||
* <strong>Key Rule:</strong> Never suggests a route that would leave a score of 1, as
|
||||
* this is impossible to finish (no double equals 1).
|
||||
* </p>
|
||||
* <p>
|
||||
* <strong>Example Suggestions:</strong>
|
||||
* <pre>
|
||||
* getRoute(32, 1) → "D16" (Direct double)
|
||||
* getRoute(50, 1) → "BULL" (Bullseye)
|
||||
* getRoute(40, 2) → "D20" (Direct double)
|
||||
* getRoute(41, 2) → "1 • D20" (Setup dart to leave 40)
|
||||
* getRoute(170, 3) → "T20 • T20 • BULL" (Pre-calculated route)
|
||||
* getRoute(100, 2) → "T20 Route" (General high-score advice)
|
||||
* </pre>
|
||||
* </p>
|
||||
* <p>
|
||||
* <strong>Design Pattern:</strong>
|
||||
* This is a pure static utility class with no instance state. All methods are
|
||||
* static and thread-safe. The checkout map is initialized once in a static block.
|
||||
* </p>
|
||||
*
|
||||
* @see #getRoute(int, int)
|
||||
*/
|
||||
private static class CheckoutEngine {
|
||||
|
||||
/**
|
||||
* Returns optimal checkout route for given score and darts remaining.
|
||||
* Considers direct doubles, setup darts, and pre-calculated routes.
|
||||
*
|
||||
* @param score Target score to finish
|
||||
* @param dartsLeft Number of darts remaining (1-3)
|
||||
* @return Checkout route string or null if unavailable
|
||||
*/
|
||||
public static String getRoute(final int score, final int dartsLeft) {
|
||||
// 1. Direct Out check (highest priority)
|
||||
if (score <= DartsConstants.MAX_DIRECT_DOUBLE && score % 2 == 0) {
|
||||
return DartsConstants.PREFIX_DOUBLE + (score / 2);
|
||||
}
|
||||
if (score == DartsConstants.DOUBLE_BULL_VALUE) return DartsConstants.LABEL_BULLSEYE;
|
||||
|
||||
// 2. Logic for Setup Darts (preventing score of 1)
|
||||
if (dartsLeft >= 2) {
|
||||
// Example: Score is 7. Suggesting D3 leaves 1 (Bust).
|
||||
// Suggesting 1 leaves 6 (D3). Correct.
|
||||
if (score <= 41 && score % 2 != 0) {
|
||||
// Try to leave a common double (32, 40, 16)
|
||||
if (score - DartsConstants.SETUP_DOUBLE_32 > 0 && score - DartsConstants.SETUP_DOUBLE_32 <= DartsConstants.MAX_DARTBOARD_NUMBER) {
|
||||
return (score - DartsConstants.SETUP_DOUBLE_32) + DartsConstants.CHECKOUT_SEPARATOR + DartsConstants.PREFIX_DOUBLE + "16";
|
||||
}
|
||||
if (score - DartsConstants.SETUP_DOUBLE_40 > 0 && score - DartsConstants.SETUP_DOUBLE_40 <= DartsConstants.MAX_DARTBOARD_NUMBER) {
|
||||
return (score - DartsConstants.SETUP_DOUBLE_40) + DartsConstants.CHECKOUT_SEPARATOR + DartsConstants.PREFIX_DOUBLE + "20";
|
||||
}
|
||||
return "1" + DartsConstants.CHECKOUT_SEPARATOR + DartsConstants.PREFIX_DOUBLE + ((score - 1) / 2); // Default setup
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback to Map or High Scoring Route
|
||||
if (CheckoutConstants.hasCheckoutRoute(score)) {
|
||||
String[] parts = CheckoutConstants.getCheckoutRoute(score);
|
||||
if (parts != null && parts.length <= dartsLeft) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
sb.append(parts[i]);
|
||||
if (i < parts.length - 1) sb.append(DartsConstants.CHECKOUT_SEPARATOR);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (score > DartsConstants.HIGH_SCORE_THRESHOLD) return DartsConstants.LABEL_T20_ROUTE;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.aldo.apps.ochecompanion;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -10,6 +12,7 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
@@ -47,6 +50,11 @@ public class MainMenuActivity extends AppCompatActivity {
|
||||
*/
|
||||
private int mTestCounter = 0;
|
||||
|
||||
/**
|
||||
* The {@link SharedPreferences} containing the currently selected settings.
|
||||
*/
|
||||
private SharedPreferences mSettingsPref;
|
||||
|
||||
/**
|
||||
* Initializes the activity: enables edge-to-edge display, configures window insets,
|
||||
* and sets up the match recap view with test data click listener.
|
||||
@@ -60,6 +68,7 @@ public class MainMenuActivity extends AppCompatActivity {
|
||||
// Enable edge-to-edge display for immersive UI experience
|
||||
EdgeToEdge.enable(this);
|
||||
setContentView(R.layout.activity_main);
|
||||
mSettingsPref = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
|
||||
// Configure window insets to properly handle system bars (status bar, navigation bar)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
|
||||
@@ -69,16 +78,14 @@ public class MainMenuActivity extends AppCompatActivity {
|
||||
});
|
||||
|
||||
findViewById(R.id.quick_start_btn).setOnClickListener(v -> quickStart());
|
||||
findViewById(R.id.btnSettings).setOnClickListener(v -> launchSettings());
|
||||
|
||||
// Set up match recap view with test data functionality
|
||||
mMatchRecap = findViewById(R.id.match_recap);
|
||||
mMatchRecap.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
mMatchRecap.setOnClickListener(v -> {
|
||||
// Cycle through test data scenarios on each click
|
||||
applyTestData(mTestCounter);
|
||||
mTestCounter++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,6 +97,16 @@ public class MainMenuActivity extends AppCompatActivity {
|
||||
super.onResume();
|
||||
// Refresh the squad view with latest player data
|
||||
initSquadView();
|
||||
// Apply the last finished match if available.
|
||||
applyLastMatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the settings activity when the settings button is clicked.
|
||||
*/
|
||||
private void launchSettings() {
|
||||
final Intent intent = new Intent(MainMenuActivity.this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,6 +161,20 @@ public class MainMenuActivity extends AppCompatActivity {
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the last completed match from the database to the match recap view.
|
||||
*/
|
||||
private void applyLastMatch() {
|
||||
// Database operations must be run on a background thread to keep the UI responsive.
|
||||
new Thread(() -> {
|
||||
final Match lastMatch = AppDatabase.getDatabase(getApplicationContext())
|
||||
.matchDao()
|
||||
.getLastCompletedMatch();
|
||||
|
||||
// Post-database query UI updates must happen back on the main (UI) thread
|
||||
runOnUiThread(() -> mMatchRecap.setMatch(lastMatch));
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies test data to the match recap view for development and testing.
|
||||
@@ -153,6 +184,7 @@ public class MainMenuActivity extends AppCompatActivity {
|
||||
* @param counter Counter value used to determine which test scenario to display.
|
||||
*/
|
||||
private void applyTestData(final int counter) {
|
||||
Log.d(TAG, "applyTestData: Applying Test Data [" + counter + "]");
|
||||
// Create test player objects
|
||||
final Player playerOne = new Player(DartsConstants.TEST_PLAYER_1, null);
|
||||
playerOne.id = 1;
|
||||
@@ -176,16 +208,22 @@ public class MainMenuActivity extends AppCompatActivity {
|
||||
|
||||
// Create test match objects with different player configurations and scores
|
||||
final Match match1on1 = new Match("501", java.util.Arrays.asList(playerOne, playerTwo), scores1v1);
|
||||
match1on1.markCompleted(); // Mark as completed for test data
|
||||
|
||||
final Match matchGroup = new Match("501", java.util.Arrays.asList(playerOne, playerTwo, playerThree, playerFour), scoresGroup);
|
||||
matchGroup.markCompleted(); // Mark as completed for test data
|
||||
|
||||
// Cycle through different test scenarios based on counter value
|
||||
if (counter % UIConstants.TEST_CYCLE_MODULO == 0) {
|
||||
Log.d(TAG, "applyTestData: No recent match selected.");
|
||||
// Scenario 1: No match (null state)
|
||||
mMatchRecap.setMatch(null);
|
||||
} else if (counter % UIConstants.TEST_CYCLE_MODULO == 1) {
|
||||
Log.d(TAG, "applyTestData: 1 on 1 Match");
|
||||
// Scenario 2: 1v1 match (two players)
|
||||
mMatchRecap.setMatch(match1on1);
|
||||
} else {
|
||||
Log.d(TAG, "applyTestData: Group Match.");
|
||||
// Scenario 3: Group match (four players)
|
||||
mMatchRecap.setMatch(matchGroup);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.aldo.apps.ochecompanion;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.aldo.apps.ochecompanion.ui.MainMenuPreferencesFragment;
|
||||
|
||||
/**
|
||||
* Settings activity for configuring app-wide preferences.
|
||||
* Hosts the MainMenuPreferencesFragment which displays available settings
|
||||
* including day/night mode and standard game mode selection.
|
||||
*/
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
|
||||
/**
|
||||
* Initializes the settings activity and loads the preferences fragment.
|
||||
* Configures window insets and enables the back button in the action bar.
|
||||
*
|
||||
* @param savedInstanceState Bundle containing saved state, or null if none exists
|
||||
*/
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.settings_activity);
|
||||
// Configure window insets to properly handle system bars (status bar, navigation bar)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
|
||||
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
|
||||
return insets;
|
||||
});
|
||||
if (savedInstanceState == null) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, new MainMenuPreferencesFragment())
|
||||
.commit();
|
||||
}
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,10 @@ import androidx.room.RoomDatabase;
|
||||
|
||||
import com.aldo.apps.ochecompanion.database.dao.MatchDao;
|
||||
import com.aldo.apps.ochecompanion.database.dao.PlayerDao;
|
||||
import com.aldo.apps.ochecompanion.database.dao.StatisticsDao;
|
||||
import com.aldo.apps.ochecompanion.database.objects.Match;
|
||||
import com.aldo.apps.ochecompanion.database.objects.Player;
|
||||
import com.aldo.apps.ochecompanion.database.objects.Statistics;
|
||||
|
||||
/**
|
||||
* Main Room database class for the Oche Companion darts application.
|
||||
@@ -21,7 +23,7 @@ import com.aldo.apps.ochecompanion.database.objects.Player;
|
||||
* @see Player
|
||||
* @see Match
|
||||
*/
|
||||
@Database(entities = {Player.class, Match.class}, version = 3, exportSchema = false)
|
||||
@Database(entities = {Player.class, Match.class, Statistics.class}, version = 10, exportSchema = false)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
||||
/**
|
||||
@@ -42,6 +44,15 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||
*/
|
||||
public abstract MatchDao matchDao();
|
||||
|
||||
/**
|
||||
* Returns the StatisticsDao for managing statistics records for players.
|
||||
* Thread-safe and can be reused. Operations must run on background threads.
|
||||
*
|
||||
* @return StatisticsDao instance for player stats database operations
|
||||
* @see StatisticsDao
|
||||
*/
|
||||
public abstract StatisticsDao statisticsDao();
|
||||
|
||||
/**
|
||||
* Singleton instance of the AppDatabase.
|
||||
* Volatile ensures thread-safe visibility in double-checked locking pattern.
|
||||
|
||||
@@ -39,4 +39,41 @@ public interface MatchDao {
|
||||
*/
|
||||
@Query("SELECT * FROM matches ORDER BY timestamp DESC LIMIT 1")
|
||||
Match getLastMatch();
|
||||
|
||||
/**
|
||||
* Retrieves the most recently completed match.
|
||||
* Must be called on a background thread.
|
||||
*
|
||||
* @return The most recent completed match, or null if no completed matches exist
|
||||
*/
|
||||
@Query("SELECT * FROM matches WHERE state = 'COMPLETED' ORDER BY timestamp DESC LIMIT 1")
|
||||
Match getLastCompletedMatch();
|
||||
|
||||
/**
|
||||
* Retrieves all completed matches ordered by most recent first.
|
||||
* Must be called on a background thread.
|
||||
*
|
||||
* @return List of completed matches sorted by timestamp descending
|
||||
*/
|
||||
@Query("SELECT * FROM matches WHERE state = 'COMPLETED' ORDER BY timestamp DESC")
|
||||
List<Match> getCompletedMatches();
|
||||
|
||||
/**
|
||||
* Retrieves all ongoing matches.
|
||||
* Must be called on a background thread.
|
||||
*
|
||||
* @return List of ongoing matches
|
||||
*/
|
||||
@Query("SELECT * FROM matches WHERE state = 'ONGOING' ORDER BY timestamp DESC")
|
||||
List<Match> getOngoingMatches();
|
||||
|
||||
/**
|
||||
* Retrieves matches by state.
|
||||
* Must be called on a background thread.
|
||||
*
|
||||
* @param state The match state to filter by ("ONGOING", "COMPLETED", or "CANCELED")
|
||||
* @return List of matches with the specified state
|
||||
*/
|
||||
@Query("SELECT * FROM matches WHERE state = :state ORDER BY timestamp DESC")
|
||||
List<Match> getMatchesByState(final String state);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.aldo.apps.ochecompanion.database.dao;
|
||||
|
||||
import static androidx.room.OnConflictStrategy.REPLACE;
|
||||
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Delete;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.OnConflictStrategy;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Update;
|
||||
|
||||
@@ -22,8 +25,8 @@ public interface PlayerDao {
|
||||
*
|
||||
* @param player The player to insert
|
||||
*/
|
||||
@Insert
|
||||
void insert(final Player player);
|
||||
@Insert(onConflict = REPLACE)
|
||||
long insert(final Player player);
|
||||
|
||||
/**
|
||||
* Updates an existing player in the database.
|
||||
@@ -53,7 +56,7 @@ public interface PlayerDao {
|
||||
* @return The player, or null if not found
|
||||
*/
|
||||
@Query("SELECT * FROM players WHERE id = :id LIMIT 1")
|
||||
Player getPlayerById(final int id);
|
||||
Player getPlayerById(final long id);
|
||||
|
||||
/**
|
||||
* Retrieves all players ordered alphabetically by username.
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.aldo.apps.ochecompanion.database.dao;
|
||||
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Delete;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Update;
|
||||
|
||||
import com.aldo.apps.ochecompanion.database.objects.Statistics;
|
||||
|
||||
/**
|
||||
* Data Access Object for Statistics entities in the Room database.
|
||||
* Provides CRUD operations for managing player performance statistics.
|
||||
* All operations must be executed on background threads.
|
||||
*/
|
||||
@Dao
|
||||
public interface StatisticsDao {
|
||||
|
||||
/**
|
||||
* Inserts a new statistics record into the database.
|
||||
* Must be called on a background thread.
|
||||
*
|
||||
* @param statistics The statistics entity to persist
|
||||
*/
|
||||
@Insert
|
||||
void insertStatistics(final Statistics statistics);
|
||||
|
||||
/**
|
||||
* Retrieves statistics for a specific player by their ID.
|
||||
* Must be called on a background thread.
|
||||
*
|
||||
* @param playerId The unique player ID
|
||||
* @return Statistics record for the player, or null if not found
|
||||
*/
|
||||
@Query("SELECT * FROM statistics WHERE playerId = :playerId")
|
||||
Statistics getStatisticsForPlayer(final long playerId);
|
||||
|
||||
/**
|
||||
* Updates an existing statistics record in the database.
|
||||
* Statistics is identified by its primary key (playerId).
|
||||
* Must be called on a background thread.
|
||||
*
|
||||
* @param statistics The statistics entity with updated data
|
||||
*/
|
||||
@Update
|
||||
void updateStatistics(final Statistics statistics);
|
||||
|
||||
/**
|
||||
* Deletes a statistics record from the database.
|
||||
* Must be called on a background thread.
|
||||
*
|
||||
* @param statistics The statistics entity to delete
|
||||
*/
|
||||
@Delete
|
||||
void deleteStatistics(final Statistics statistics);
|
||||
}
|
||||
@@ -13,11 +13,11 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represents a completed darts match in the Oche Companion application.
|
||||
* Represents a darts match in the Oche Companion application.
|
||||
* Room entity storing match information including game mode, timestamp, player count,
|
||||
* and detailed performance data for all participants. Implements Serializable for
|
||||
* passing between Android components. Provides helper methods to parse participant
|
||||
* JSON data and reconstruct Player objects.
|
||||
* state (ongoing/completed/canceled), and detailed performance data for all participants.
|
||||
* Implements Serializable for passing between Android components. Provides helper methods
|
||||
* to parse participant JSON data and reconstruct Player objects.
|
||||
*
|
||||
* @see com.aldo.apps.ochecompanion.database.dao.MatchDao
|
||||
* @see com.aldo.apps.ochecompanion.database.objects.Player
|
||||
@@ -27,6 +27,20 @@ import java.util.Map;
|
||||
@Entity(tableName = "matches")
|
||||
public class Match implements Serializable {
|
||||
|
||||
/**
|
||||
* Represents the current state of a match.
|
||||
*/
|
||||
public enum MatchState {
|
||||
/** Match is currently in progress */
|
||||
ONGOING,
|
||||
|
||||
/** Match has been completed successfully */
|
||||
COMPLETED,
|
||||
|
||||
/** Match was canceled before completion */
|
||||
CANCELED
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-generated unique primary key for this match.
|
||||
* Value is 0 before insertion, then assigned by Room using SQLite AUTOINCREMENT.
|
||||
@@ -67,27 +81,35 @@ public class Match implements Serializable {
|
||||
*/
|
||||
public String participantData;
|
||||
|
||||
/**
|
||||
* Current state of the match (ONGOING, COMPLETED, or CANCELED).
|
||||
* New matches start as ONGOING and transition to COMPLETED or CANCELED.
|
||||
*/
|
||||
public MatchState state;
|
||||
|
||||
/**
|
||||
* Constructs a new Match entity ready for database insertion.
|
||||
* The match ID will be auto-generated by Room upon insertion.
|
||||
*
|
||||
* @param timestamp Unix epoch timestamp in milliseconds when match completed
|
||||
* @param timestamp Unix epoch timestamp in milliseconds when match was created/completed
|
||||
* @param gameMode Identifier for the darts game variant (e.g., "501", "Cricket")
|
||||
* @param playerCount Number of players who participated (must be at least 1)
|
||||
* @param participantData JSON string containing player performance data
|
||||
* @param state Current state of the match (ONGOING, COMPLETED, or CANCELED)
|
||||
* @see com.aldo.apps.ochecompanion.database.dao.MatchDao#insert(Match)
|
||||
*/
|
||||
public Match(final long timestamp, final String gameMode, final int playerCount, final String participantData) {
|
||||
public Match(final long timestamp, final String gameMode, final int playerCount, final String participantData, final MatchState state) {
|
||||
this.timestamp = timestamp;
|
||||
this.gameMode = gameMode;
|
||||
this.playerCount = playerCount;
|
||||
this.participantData = participantData;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience constructor for creating a Match from a list of Player objects.
|
||||
* Automatically generates JSON participant data and sets timestamp to current time.
|
||||
* All players will have a score of 0.
|
||||
* All players will have a score of 0. Match state is set to ONGOING.
|
||||
*
|
||||
* @param gameMode Identifier for the darts game variant (e.g., "501", "Cricket")
|
||||
* @param players List of Player objects to include in this match
|
||||
@@ -98,11 +120,13 @@ public class Match implements Serializable {
|
||||
this.gameMode = gameMode;
|
||||
this.playerCount = players.size();
|
||||
this.participantData = generateParticipantJson(players, null);
|
||||
this.state = MatchState.ONGOING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience constructor for creating a Match from players with their scores.
|
||||
* Automatically generates JSON participant data and sets timestamp to current time.
|
||||
* Match state is set to ONGOING by default.
|
||||
*
|
||||
* @param gameMode Identifier for the darts game variant (e.g., "501", "Cricket")
|
||||
* @param players List of Player objects to include in this match
|
||||
@@ -114,6 +138,7 @@ public class Match implements Serializable {
|
||||
this.gameMode = gameMode;
|
||||
this.playerCount = players.size();
|
||||
this.participantData = generateParticipantJson(players, scores);
|
||||
this.state = MatchState.ONGOING;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,4 +318,46 @@ public class Match implements Serializable {
|
||||
}
|
||||
return participants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this match as completed and updates the timestamp.
|
||||
*/
|
||||
public void markCompleted() {
|
||||
this.state = MatchState.COMPLETED;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this match as canceled.
|
||||
*/
|
||||
public void markCanceled() {
|
||||
this.state = MatchState.CANCELED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the match is currently ongoing.
|
||||
*
|
||||
* @return true if the match is in ONGOING state
|
||||
*/
|
||||
public boolean isOngoing() {
|
||||
return this.state == MatchState.ONGOING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the match is completed.
|
||||
*
|
||||
* @return true if the match is in COMPLETED state
|
||||
*/
|
||||
public boolean isCompleted() {
|
||||
return this.state == MatchState.COMPLETED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the match was canceled.
|
||||
*
|
||||
* @return true if the match is in CANCELED state
|
||||
*/
|
||||
public boolean isCanceled() {
|
||||
return this.state == MatchState.CANCELED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public class Player implements Parcelable {
|
||||
* Room auto-populates on insert.
|
||||
*/
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
public int id;
|
||||
public long id;
|
||||
|
||||
/**
|
||||
* Player's display name shown throughout the app. Should be non-null and
|
||||
@@ -66,7 +66,7 @@ public class Player implements Parcelable {
|
||||
* @param in Parcel containing serialized Player data
|
||||
*/
|
||||
protected Player(final Parcel in) {
|
||||
id = in.readInt();
|
||||
id = in.readLong();
|
||||
username = in.readString();
|
||||
profilePictureUri = in.readString();
|
||||
careerAverage = in.readDouble();
|
||||
@@ -101,7 +101,7 @@ public class Player implements Parcelable {
|
||||
*/
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeInt(id);
|
||||
dest.writeLong(id);
|
||||
dest.writeString(username);
|
||||
dest.writeString(profilePictureUri);
|
||||
dest.writeDouble(careerAverage);
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
package com.aldo.apps.ochecompanion.database.objects;
|
||||
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.ForeignKey;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
/**
|
||||
* Room database entity representing player performance statistics.
|
||||
* Tracks comprehensive career metrics including darts thrown, accuracy,
|
||||
* matches played, and double-out success rates. Each player has exactly
|
||||
* one Statistics record, identified by the player's ID.
|
||||
*
|
||||
* @see com.aldo.apps.ochecompanion.database.dao.StatisticsDao
|
||||
* @see Player
|
||||
*/
|
||||
@Entity(tableName = "statistics")
|
||||
public class Statistics {
|
||||
|
||||
/**
|
||||
* Player ID this statistics record belongs to (primary key).
|
||||
* References the Player entity's ID field.
|
||||
*/
|
||||
@PrimaryKey
|
||||
private long playerId;
|
||||
|
||||
/**
|
||||
* Total number of darts thrown across all matches.
|
||||
* Used to calculate career average and accuracy metrics.
|
||||
*/
|
||||
private long dartsThrown;
|
||||
|
||||
/**
|
||||
* Total number of darts that missed the intended target.
|
||||
* Contributes to accuracy calculations and player skill assessment.
|
||||
*/
|
||||
private long dartsMissed;
|
||||
|
||||
/**
|
||||
* Cumulative points scored across all darts thrown.
|
||||
* Used with dartsThrown to calculate career average.
|
||||
*/
|
||||
private long overallPointsMade;
|
||||
|
||||
/**
|
||||
* Total number of completed matches for this player.
|
||||
* Provides context for statistical significance.
|
||||
*/
|
||||
private int matchesPlayed;
|
||||
|
||||
/**
|
||||
* Total number of double-out attempts (both successful and missed).
|
||||
* Tracks how often player attempts to finish on doubles.
|
||||
*/
|
||||
private int doubleOutsTargeted;
|
||||
|
||||
/**
|
||||
* Number of failed double-out attempts.
|
||||
* Used to calculate double-out success rate.
|
||||
*/
|
||||
private int doubleOutsMissed;
|
||||
|
||||
/**
|
||||
* Constructs a new Statistics record for a player.
|
||||
* Initializes all counters to zero.
|
||||
*
|
||||
* @param playerId The unique player ID this statistics belongs to
|
||||
*/
|
||||
public Statistics(final long playerId) {
|
||||
this.playerId = playerId;
|
||||
dartsThrown = 0;
|
||||
overallPointsMade = 0;
|
||||
matchesPlayed = 0;
|
||||
doubleOutsTargeted = 0;
|
||||
doubleOutsMissed = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records darts thrown in a turn with their point value.
|
||||
*
|
||||
* @param dartsThrown Number of darts thrown in the turn
|
||||
* @param pointsMade Total points scored with those darts
|
||||
*/
|
||||
public void saveDartsThrown(final long dartsThrown, final long pointsMade) {
|
||||
this.dartsThrown += dartsThrown;
|
||||
this.overallPointsMade += pointsMade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds missed darts to the statistics.
|
||||
* Used when a player doesn't throw all 3 darts in a turn (e.g., checkout or bust).
|
||||
*
|
||||
* @param dartsMissed Number of darts not thrown
|
||||
*/
|
||||
public void addMissedDarts(final long dartsMissed) {
|
||||
this.dartsMissed += dartsMissed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the completed matches counter.
|
||||
* Should be called when a match finishes.
|
||||
*/
|
||||
public void addCompletedMatch() {
|
||||
matchesPlayed++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a double-out attempt.
|
||||
*
|
||||
* @param isMissed true if the double-out was missed, false if successful
|
||||
*/
|
||||
public void addDoubleOutTarget(final boolean isMissed) {
|
||||
doubleOutsTargeted++;
|
||||
if (isMissed) {
|
||||
doubleOutsMissed++;
|
||||
}
|
||||
}
|
||||
|
||||
public long getPlayerId() {
|
||||
return playerId;
|
||||
}
|
||||
|
||||
public void setPlayerId(long playerId) {
|
||||
this.playerId = playerId;
|
||||
}
|
||||
|
||||
public long getDartsThrown() {
|
||||
return dartsThrown;
|
||||
}
|
||||
|
||||
public void setDartsThrown(long dartsThrown) {
|
||||
this.dartsThrown = dartsThrown;
|
||||
}
|
||||
|
||||
public long getDartsMissed() {
|
||||
return dartsMissed;
|
||||
}
|
||||
|
||||
public void setDartsMissed(long dartsMissed) {
|
||||
this.dartsMissed = dartsMissed;
|
||||
}
|
||||
|
||||
public long getOverallPointsMade() {
|
||||
return overallPointsMade;
|
||||
}
|
||||
|
||||
public void setOverallPointsMade(long overallPointsMade) {
|
||||
this.overallPointsMade = overallPointsMade;
|
||||
}
|
||||
|
||||
public int getMatchesPlayed() {
|
||||
return matchesPlayed;
|
||||
}
|
||||
|
||||
public void setMatchesPlayed(int matchesPlayed) {
|
||||
this.matchesPlayed = matchesPlayed;
|
||||
}
|
||||
|
||||
public int getDoubleOutsTargeted() {
|
||||
return doubleOutsTargeted;
|
||||
}
|
||||
|
||||
public void setDoubleOutsTargeted(int doubleOutsTargeted) {
|
||||
this.doubleOutsTargeted = doubleOutsTargeted;
|
||||
}
|
||||
|
||||
public int getDoubleOutsMissed() {
|
||||
return doubleOutsMissed;
|
||||
}
|
||||
|
||||
public void setDoubleOutsMissed(int doubleOutsMissed) {
|
||||
this.doubleOutsMissed = doubleOutsMissed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Statistics{" +
|
||||
"playerId=" + playerId +
|
||||
", dartsThrown=" + dartsThrown +
|
||||
", dartsMissed=" + dartsMissed +
|
||||
", overallPointsMade=" + overallPointsMade +
|
||||
", matchesPlayed=" + matchesPlayed +
|
||||
", doubleOutsTargeted=" + doubleOutsTargeted +
|
||||
", doubleOutsMissed=" + doubleOutsMissed +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.aldo.apps.ochecompanion.ui;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.aldo.apps.ochecompanion.R;
|
||||
|
||||
/**
|
||||
* Preference fragment for the main menu settings screen.
|
||||
* Displays app-wide preferences including day/night mode and standard game mode selection.
|
||||
* Preferences are automatically persisted to SharedPreferences by the AndroidX Preference library.
|
||||
*/
|
||||
public class MainMenuPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
/**
|
||||
* Initializes the preference screen from the main_menu_preferences XML resource.
|
||||
* Called automatically by the fragment lifecycle.
|
||||
*
|
||||
* @param savedInstanceState Bundle containing saved state, or null if none exists
|
||||
* @param rootKey Optional preference hierarchy root key
|
||||
*/
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
setPreferencesFromResource(R.xml.main_menu_preferences, rootKey);
|
||||
}
|
||||
}
|
||||
@@ -1,80 +1,191 @@
|
||||
package com.aldo.apps.ochecompanion.utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Pre-calculated checkout routes for classic darts finishes.
|
||||
* Contains standard 3-dart checkout combinations for common scores.
|
||||
* Pre-calculated checkout routes for all possible double-out combinations.
|
||||
* Generates all valid 1-dart, 2-dart, and 3-dart checkouts during class initialization.
|
||||
* Stores multiple routes per score for flexibility, with optimal route selection.
|
||||
*/
|
||||
public final class CheckoutConstants {
|
||||
|
||||
private static final int SINGLE = 1;
|
||||
private static final int DOUBLE = 2;
|
||||
private static final int TRIPLE = 3;
|
||||
|
||||
// Prevent instantiation
|
||||
private CheckoutConstants() {
|
||||
throw new UnsupportedOperationException("Constants class cannot be instantiated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of pre-calculated checkout routes.
|
||||
* Key: Target score, Value: Array of dart notations to achieve the checkout
|
||||
* Map of all possible checkout routes.
|
||||
* Key: Target score, Value: List of all possible routes (each route is an array of dart notations)
|
||||
*/
|
||||
private static final Map<Integer, String[]> CHECKOUT_MAP = new HashMap<>();
|
||||
private static final Map<Integer, List<String[]>> ALL_CHECKOUTS = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Map of optimal checkout routes (best route per score).
|
||||
* Key: Target score, Value: Optimal dart notation array
|
||||
*/
|
||||
private static final Map<Integer, String[]> OPTIMAL_CHECKOUTS = new HashMap<>();
|
||||
|
||||
static {
|
||||
// Maximum 3-dart checkouts
|
||||
CHECKOUT_MAP.put(170, new String[]{"T20", "T20", "BULL"});
|
||||
CHECKOUT_MAP.put(167, new String[]{"T20", "T19", "BULL"});
|
||||
CHECKOUT_MAP.put(164, new String[]{"T20", "T18", "BULL"});
|
||||
CHECKOUT_MAP.put(161, new String[]{"T20", "T17", "BULL"});
|
||||
CHECKOUT_MAP.put(160, new String[]{"T20", "T20", "D20"});
|
||||
|
||||
// Common high finishes
|
||||
CHECKOUT_MAP.put(141, new String[]{"T20", "T19", "D12"});
|
||||
CHECKOUT_MAP.put(140, new String[]{"T20", "T20", "D10"});
|
||||
CHECKOUT_MAP.put(139, new String[]{"T20", "T19", "D11"});
|
||||
CHECKOUT_MAP.put(138, new String[]{"T20", "T18", "D12"});
|
||||
CHECKOUT_MAP.put(137, new String[]{"T20", "T19", "D10"});
|
||||
CHECKOUT_MAP.put(136, new String[]{"T20", "T20", "D8"});
|
||||
CHECKOUT_MAP.put(135, new String[]{"T20", "T17", "D12"});
|
||||
CHECKOUT_MAP.put(134, new String[]{"T20", "T14", "D16"});
|
||||
CHECKOUT_MAP.put(133, new String[]{"T20", "T19", "D8"});
|
||||
CHECKOUT_MAP.put(132, new String[]{"T20", "T16", "D12"});
|
||||
CHECKOUT_MAP.put(131, new String[]{"T20", "T13", "D16"});
|
||||
CHECKOUT_MAP.put(130, new String[]{"T20", "T20", "D5"});
|
||||
|
||||
// Mid-range finishes
|
||||
CHECKOUT_MAP.put(121, new String[]{"T17", "T18", "D10"});
|
||||
CHECKOUT_MAP.put(120, new String[]{"T20", "20", "D20"});
|
||||
CHECKOUT_MAP.put(119, new String[]{"T19", "T12", "D13"});
|
||||
CHECKOUT_MAP.put(118, new String[]{"T18", "T14", "D14"});
|
||||
CHECKOUT_MAP.put(117, new String[]{"T20", "17", "D20"});
|
||||
CHECKOUT_MAP.put(116, new String[]{"T19", "19", "D20"});
|
||||
CHECKOUT_MAP.put(115, new String[]{"T19", "18", "D20"});
|
||||
CHECKOUT_MAP.put(114, new String[]{"T18", "18", "D20"});
|
||||
CHECKOUT_MAP.put(113, new String[]{"T19", "16", "D20"});
|
||||
CHECKOUT_MAP.put(112, new String[]{"T20", "12", "D20"});
|
||||
CHECKOUT_MAP.put(111, new String[]{"T20", "11", "D20"});
|
||||
CHECKOUT_MAP.put(110, new String[]{"T20", "10", "D20"});
|
||||
|
||||
// Lower finishes
|
||||
CHECKOUT_MAP.put(107, new String[]{"T19", "10", "D20"});
|
||||
CHECKOUT_MAP.put(106, new String[]{"T20", "10", "D18"});
|
||||
CHECKOUT_MAP.put(105, new String[]{"T20", "13", "D16"});
|
||||
CHECKOUT_MAP.put(104, new String[]{"T18", "18", "D16"});
|
||||
CHECKOUT_MAP.put(103, new String[]{"T17", "12", "D20"});
|
||||
CHECKOUT_MAP.put(102, new String[]{"T20", "10", "D16"});
|
||||
CHECKOUT_MAP.put(101, new String[]{"T17", "10", "D20"});
|
||||
CHECKOUT_MAP.put(100, new String[]{"T20", "D20"});
|
||||
generateAllCheckouts();
|
||||
selectOptimalCheckouts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the pre-calculated checkout route for a given score.
|
||||
* Generates all possible 1-dart, 2-dart, and 3-dart double-out combinations.
|
||||
*/
|
||||
private static void generateAllCheckouts() {
|
||||
// Generate 1-dart checkouts (D1-D20, plus Bull)
|
||||
for (int first = 1; first <= 20; first++) {
|
||||
final int score = first * DOUBLE;
|
||||
addCheckout(score, new String[]{DartsConstants.PREFIX_DOUBLE + first});
|
||||
}
|
||||
addCheckout(50, new String[]{DartsConstants.LABEL_BULLSEYE});
|
||||
|
||||
// Generate 2-dart checkouts
|
||||
for (int doubleOut = 1; doubleOut <= 20; doubleOut++) {
|
||||
final int outValue = doubleOut * DOUBLE;
|
||||
|
||||
for (int setup = 1; setup <= 20; setup++) {
|
||||
// Single + Double
|
||||
final int singleScore = (setup * SINGLE) + outValue;
|
||||
addCheckout(singleScore, new String[]{String.valueOf(setup), DartsConstants.PREFIX_DOUBLE + doubleOut});
|
||||
|
||||
// Double + Double
|
||||
final int doubleScore = (setup * DOUBLE) + outValue;
|
||||
addCheckout(doubleScore, new String[]{DartsConstants.PREFIX_DOUBLE + setup, DartsConstants.PREFIX_DOUBLE + doubleOut});
|
||||
|
||||
// Triple + Double
|
||||
final int tripleScore = (setup * TRIPLE) + outValue;
|
||||
addCheckout(tripleScore, new String[]{DartsConstants.PREFIX_TRIPLE + setup, DartsConstants.PREFIX_DOUBLE + doubleOut});
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 3-dart checkouts
|
||||
for (int doubleOut = 1; doubleOut <= 20; doubleOut++) {
|
||||
final int outValue = doubleOut * DOUBLE;
|
||||
|
||||
for (int second = 1; second <= 20; second++) {
|
||||
final int singleSecond = second * SINGLE;
|
||||
final int doubleSecond = second * DOUBLE;
|
||||
final int tripleSecond = second * TRIPLE;
|
||||
|
||||
for (int first = 1; first <= 20; first++) {
|
||||
// All 9 combinations of (single/double/triple) + (single/double/triple) + double
|
||||
addCheckout((first * SINGLE) + singleSecond + outValue,
|
||||
new String[]{String.valueOf(first), String.valueOf(second), DartsConstants.PREFIX_DOUBLE + doubleOut});
|
||||
addCheckout((first * SINGLE) + doubleSecond + outValue,
|
||||
new String[]{String.valueOf(first), DartsConstants.PREFIX_DOUBLE + second, DartsConstants.PREFIX_DOUBLE + doubleOut});
|
||||
addCheckout((first * SINGLE) + tripleSecond + outValue,
|
||||
new String[]{String.valueOf(first), DartsConstants.PREFIX_TRIPLE + second, DartsConstants.PREFIX_DOUBLE + doubleOut});
|
||||
|
||||
addCheckout((first * DOUBLE) + singleSecond + outValue,
|
||||
new String[]{DartsConstants.PREFIX_DOUBLE + first, String.valueOf(second), DartsConstants.PREFIX_DOUBLE + doubleOut});
|
||||
addCheckout((first * DOUBLE) + doubleSecond + outValue,
|
||||
new String[]{DartsConstants.PREFIX_DOUBLE + first, DartsConstants.PREFIX_DOUBLE + second, DartsConstants.PREFIX_DOUBLE + doubleOut});
|
||||
addCheckout((first * DOUBLE) + tripleSecond + outValue,
|
||||
new String[]{DartsConstants.PREFIX_DOUBLE + first, DartsConstants.PREFIX_TRIPLE + second, DartsConstants.PREFIX_DOUBLE + doubleOut});
|
||||
|
||||
addCheckout((first * TRIPLE) + singleSecond + outValue,
|
||||
new String[]{DartsConstants.PREFIX_TRIPLE + first, String.valueOf(second), DartsConstants.PREFIX_DOUBLE + doubleOut});
|
||||
addCheckout((first * TRIPLE) + doubleSecond + outValue,
|
||||
new String[]{DartsConstants.PREFIX_TRIPLE + first, DartsConstants.PREFIX_DOUBLE + second, DartsConstants.PREFIX_DOUBLE + doubleOut});
|
||||
addCheckout((first * TRIPLE) + tripleSecond + outValue,
|
||||
new String[]{DartsConstants.PREFIX_TRIPLE + first, DartsConstants.PREFIX_TRIPLE + second, DartsConstants.PREFIX_DOUBLE + doubleOut});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a checkout route to the map.
|
||||
*/
|
||||
private static void addCheckout(final int score, final String[] route) {
|
||||
ALL_CHECKOUTS.computeIfAbsent(score, k -> new ArrayList<>()).add(route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the optimal route for each score based on preferred strategies.
|
||||
* Preferences: Fewer darts > T20/T19 usage > Higher finishing double
|
||||
*/
|
||||
private static void selectOptimalCheckouts() {
|
||||
for (Map.Entry<Integer, List<String[]>> entry : ALL_CHECKOUTS.entrySet()) {
|
||||
final int score = entry.getKey();
|
||||
final List<String[]> routes = entry.getValue();
|
||||
|
||||
String[] bestRoute = routes.get(0);
|
||||
int bestScore = scoreRoute(bestRoute);
|
||||
|
||||
for (String[] route : routes) {
|
||||
final int routeScore = scoreRoute(route);
|
||||
if (routeScore > bestScore) {
|
||||
bestScore = routeScore;
|
||||
bestRoute = route;
|
||||
}
|
||||
}
|
||||
|
||||
OPTIMAL_CHECKOUTS.put(score, bestRoute);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scores a route based on desirability.
|
||||
* Higher score = better route
|
||||
*/
|
||||
private static int scoreRoute(final String[] route) {
|
||||
int score = 0;
|
||||
|
||||
// Prefer fewer darts (most important)
|
||||
score += (4 - route.length) * 10000;
|
||||
|
||||
// Prefer T20 (second most important)
|
||||
for (String dart : route) {
|
||||
if ("T20".equals(dart)) score += 100;
|
||||
else if ("T19".equals(dart)) score += 90;
|
||||
else if (dart.startsWith("T")) score += 50;
|
||||
}
|
||||
|
||||
// Prefer higher finishing doubles
|
||||
final String lastDart = route[route.length - 1];
|
||||
if (lastDart.startsWith("D")) {
|
||||
try {
|
||||
final int doubleValue = Integer.parseInt(lastDart.substring(1));
|
||||
score += doubleValue;
|
||||
} catch (NumberFormatException e) {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
} else if (DartsConstants.LABEL_BULLSEYE.equals(lastDart)) {
|
||||
score += 25; // Bull gets high score
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the optimal pre-calculated checkout route for a given score.
|
||||
*
|
||||
* @param score The target score to checkout
|
||||
* @return Array of dart notations, or null if no route exists
|
||||
*/
|
||||
public static String[] getCheckoutRoute(final int score) {
|
||||
return CHECKOUT_MAP.get(score);
|
||||
return OPTIMAL_CHECKOUTS.get(score);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all possible checkout routes for a given score.
|
||||
*
|
||||
* @param score The target score to checkout
|
||||
* @return List of all possible routes, or null if no routes exist
|
||||
*/
|
||||
public static List<String[]> getAllCheckoutRoutes(final int score) {
|
||||
return ALL_CHECKOUTS.get(score);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +195,7 @@ public final class CheckoutConstants {
|
||||
* @return true if a route exists, false otherwise
|
||||
*/
|
||||
public static boolean hasCheckoutRoute(final int score) {
|
||||
return CHECKOUT_MAP.containsKey(score);
|
||||
return OPTIMAL_CHECKOUTS.containsKey(score);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,6 +204,19 @@ public final class CheckoutConstants {
|
||||
* @return Set of scores with pre-calculated routes
|
||||
*/
|
||||
public static java.util.Set<Integer> getAvailableCheckouts() {
|
||||
return CHECKOUT_MAP.keySet();
|
||||
return OPTIMAL_CHECKOUTS.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total number of unique checkout combinations generated.
|
||||
*
|
||||
* @return Total number of routes across all scores
|
||||
*/
|
||||
public static int getTotalRoutesCount() {
|
||||
int total = 0;
|
||||
for (List<String[]> routes : ALL_CHECKOUTS.values()) {
|
||||
total += routes.size();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
package com.aldo.apps.ochecompanion.utils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Checkout calculation engine for X01 darts games.
|
||||
* Provides optimal double-out routes for any score with 1-3 darts remaining.
|
||||
* Tracks if players are following suggested paths and identifies double-out attempts.
|
||||
*/
|
||||
public final class CheckoutEngine {
|
||||
|
||||
// Prevent instantiation
|
||||
private CheckoutEngine() {
|
||||
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
||||
}
|
||||
|
||||
/** All possible double-out finishes: D1 through D20, plus Bull (50) */
|
||||
private static final int[] DOUBLE_OUT_VALUES = {
|
||||
2, 4, 6, 8, 10, 12, 14, 16, 18, 20, // D1-D10
|
||||
22, 24, 26, 28, 30, 32, 34, 36, 38, 40, // D11-D20
|
||||
50 // Double Bull
|
||||
};
|
||||
|
||||
/** Map of double values to their display labels (e.g., 40 -> "D20") */
|
||||
private static final Map<Integer, String> DOUBLE_LABELS = new HashMap<>();
|
||||
|
||||
static {
|
||||
// Initialize double labels
|
||||
for (int i = 1; i <= 20; i++) {
|
||||
DOUBLE_LABELS.put(i * 2, DartsConstants.PREFIX_DOUBLE + i);
|
||||
}
|
||||
DOUBLE_LABELS.put(50, DartsConstants.LABEL_BULLSEYE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates optimal checkout route for given score and darts remaining.
|
||||
*
|
||||
* @param score Target score to finish
|
||||
* @param dartsLeft Number of darts remaining (1-3)
|
||||
* @return Checkout route string or null if no route available
|
||||
*/
|
||||
public static String calculateCheckout(final int score, final int dartsLeft) {
|
||||
if (dartsLeft <= 0 || score <= 0 || score == 1) return null;
|
||||
|
||||
switch (dartsLeft) {
|
||||
case 1:
|
||||
return calculateOneDartDoubleOut(score);
|
||||
case 2:
|
||||
return calculateTwoDartDoubleOut(score);
|
||||
case 3:
|
||||
return calculateThreeDartDoubleOut(score);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates checkout for exactly one dart remaining.
|
||||
* Only suggests if score is a valid double (2-40 even, or 50).
|
||||
*
|
||||
* @param score Target score
|
||||
* @return Double-out dart label or null
|
||||
*/
|
||||
public static String calculateOneDartDoubleOut(final int score) {
|
||||
if (isValidDoubleOut(score)) {
|
||||
return DOUBLE_LABELS.get(score);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates checkout for exactly two darts remaining.
|
||||
* Tries: direct double, then setup dart + double.
|
||||
*
|
||||
* @param score Target score
|
||||
* @return Checkout route or null
|
||||
*/
|
||||
public static String calculateTwoDartDoubleOut(final int score) {
|
||||
// Try direct double out
|
||||
if (isValidDoubleOut(score)) {
|
||||
return DOUBLE_LABELS.get(score);
|
||||
}
|
||||
|
||||
// Check pre-calculated routes from CheckoutConstants
|
||||
if (CheckoutConstants.hasCheckoutRoute(score)) {
|
||||
final String[] parts = CheckoutConstants.getCheckoutRoute(score);
|
||||
if (parts != null && parts.length <= 2) {
|
||||
return String.join(DartsConstants.CHECKOUT_SEPARATOR, parts);
|
||||
}
|
||||
}
|
||||
|
||||
// Try setup dart logic for odd scores
|
||||
if (score % 2 != 0 && score <= 41) {
|
||||
return calculateSetupDart(score);
|
||||
}
|
||||
|
||||
// Try to leave a valid double
|
||||
for (int doubleValue : DOUBLE_OUT_VALUES) {
|
||||
final int setup = score - doubleValue;
|
||||
if (setup > 0 && setup <= 60) { // Single segment max is 20, triple is 60
|
||||
return setup + DartsConstants.CHECKOUT_SEPARATOR + DOUBLE_LABELS.get(doubleValue);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates checkout for exactly three darts remaining.
|
||||
* Tries: direct double, two-dart checkout, then three-dart combinations.
|
||||
*
|
||||
* @param score Target score
|
||||
* @return Checkout route or null
|
||||
*/
|
||||
public static String calculateThreeDartDoubleOut(final int score) {
|
||||
// Try simpler options first
|
||||
final String oneDart = calculateOneDartDoubleOut(score);
|
||||
if (oneDart != null) return oneDart;
|
||||
|
||||
final String twoDart = calculateTwoDartDoubleOut(score);
|
||||
if (twoDart != null) return twoDart;
|
||||
|
||||
// Check pre-calculated three-dart routes
|
||||
if (CheckoutConstants.hasCheckoutRoute(score)) {
|
||||
final String[] parts = CheckoutConstants.getCheckoutRoute(score);
|
||||
if (parts != null && parts.length <= 3) {
|
||||
return String.join(DartsConstants.CHECKOUT_SEPARATOR, parts);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to build a three-dart route
|
||||
// Strategy: High score + setup + double
|
||||
if (score > 50 && score <= 170) {
|
||||
// Try T20 (60) + remainder
|
||||
int remainder = score - 60;
|
||||
if (remainder > 0) {
|
||||
final String twoRemaining = calculateTwoDartDoubleOut(remainder);
|
||||
if (twoRemaining != null) {
|
||||
return "T20" + DartsConstants.CHECKOUT_SEPARATOR + twoRemaining;
|
||||
}
|
||||
}
|
||||
|
||||
// Try T19 (57) + remainder
|
||||
remainder = score - 57;
|
||||
if (remainder > 0) {
|
||||
final String twoRemaining = calculateTwoDartDoubleOut(remainder);
|
||||
if (twoRemaining != null) {
|
||||
return "T19" + DartsConstants.CHECKOUT_SEPARATOR + twoRemaining;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates optimal setup dart to leave a common double.
|
||||
* Prefers leaving D20 (40) or D16 (32).
|
||||
*
|
||||
* @param score Odd score requiring setup
|
||||
* @return Setup route (e.g., "1 • D20")
|
||||
*/
|
||||
private static String calculateSetupDart(final int score) {
|
||||
// Try to leave D20 (40)
|
||||
if (score > 40 && score - 40 <= 20) {
|
||||
return (score - 40) + DartsConstants.CHECKOUT_SEPARATOR + DartsConstants.PREFIX_DOUBLE + "20";
|
||||
}
|
||||
|
||||
// Try to leave D16 (32)
|
||||
if (score > 32 && score - 32 <= 20) {
|
||||
return (score - 32) + DartsConstants.CHECKOUT_SEPARATOR + DartsConstants.PREFIX_DOUBLE + "16";
|
||||
}
|
||||
|
||||
// General solution: throw 1 to leave an even score
|
||||
if (score > 1) {
|
||||
final int remainder = score - 1;
|
||||
if (isValidDoubleOut(remainder)) {
|
||||
return "1" + DartsConstants.CHECKOUT_SEPARATOR + DOUBLE_LABELS.get(remainder);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a score is a valid double-out finish.
|
||||
*
|
||||
* @param score Score to check
|
||||
* @return true if score can be finished with a double
|
||||
*/
|
||||
public static boolean isValidDoubleOut(final int score) {
|
||||
return DOUBLE_LABELS.containsKey(score);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a dart value represents a double-out attempt.
|
||||
* Used for tracking double attempts in statistics.
|
||||
*
|
||||
* @param dartValue Point value of the dart thrown
|
||||
* @return true if dart was a double (2-40 even, or 50)
|
||||
*/
|
||||
public static boolean isDoubleOutDart(final int dartValue) {
|
||||
return isValidDoubleOut(dartValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the first dart from a checkout route string.
|
||||
*
|
||||
* @param checkoutRoute Full route (e.g., "T20 • T20 • BULL")
|
||||
* @return First dart string (e.g., "T20") or null
|
||||
*/
|
||||
public static String getFirstDartFromRoute(final String checkoutRoute) {
|
||||
if (checkoutRoute == null || checkoutRoute.isEmpty()) return null;
|
||||
final String[] parts = checkoutRoute.split(DartsConstants.CHECKOUT_SEPARATOR);
|
||||
return parts.length > 0 ? parts[0].trim() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a thrown dart matches the suggested first dart in a route.
|
||||
* Used to determine if player is following the recommended path.
|
||||
*
|
||||
* @param checkoutRoute Suggested route (e.g., "T20 • D20")
|
||||
* @param dartThrown Point value thrown (e.g., 60)
|
||||
* @return true if dart follows the route
|
||||
*/
|
||||
public static boolean isFollowingSuggestedRoute(final String checkoutRoute, final int dartThrown) {
|
||||
final String firstDart = getFirstDartFromRoute(checkoutRoute);
|
||||
if (firstDart == null) return false;
|
||||
|
||||
// Parse the expected dart value
|
||||
final int expectedValue = parseDartValue(firstDart);
|
||||
return expectedValue == dartThrown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a dart label into its point value.
|
||||
*
|
||||
* @param dartLabel Label like "T20", "D16", "BULL", or "25"
|
||||
* @return Point value (e.g., 60, 32, 50, 25)
|
||||
*/
|
||||
private static int parseDartValue(final String dartLabel) {
|
||||
if (dartLabel == null || dartLabel.isEmpty()) return 0;
|
||||
|
||||
final String label = dartLabel.trim();
|
||||
|
||||
// Handle bullseye
|
||||
if (label.equals(DartsConstants.LABEL_BULLSEYE) || label.equals(DartsConstants.LABEL_DOUBLE_BULL)) {
|
||||
return DartsConstants.DOUBLE_BULL_VALUE;
|
||||
}
|
||||
if (label.equals(DartsConstants.LABEL_BULL)) {
|
||||
return DartsConstants.BULL_VALUE;
|
||||
}
|
||||
|
||||
// Handle prefixed darts (D16, T20, etc.)
|
||||
if (label.startsWith(DartsConstants.PREFIX_TRIPLE)) {
|
||||
try {
|
||||
final int base = Integer.parseInt(label.substring(1));
|
||||
return base * DartsConstants.MULTIPLIER_TRIPLE;
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (label.startsWith(DartsConstants.PREFIX_DOUBLE)) {
|
||||
try {
|
||||
final int base = Integer.parseInt(label.substring(1));
|
||||
return base * DartsConstants.MULTIPLIER_DOUBLE;
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Plain number
|
||||
try {
|
||||
return Integer.parseInt(label);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all possible double-out values.
|
||||
*
|
||||
* @return Array of valid double finish scores
|
||||
*/
|
||||
public static int[] getAllDoubleOutValues() {
|
||||
return DOUBLE_OUT_VALUES.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a remaining score can be finished with the given number of darts.
|
||||
*
|
||||
* @param score Remaining score
|
||||
* @param dartsLeft Darts available
|
||||
* @return true if checkout is possible
|
||||
*/
|
||||
public static boolean hasCheckoutRoute(final int score, final int dartsLeft) {
|
||||
return calculateCheckout(score, dartsLeft) != null;
|
||||
}
|
||||
}
|
||||
31
app/src/main/res/drawable/ic_day_night_mode.xml
Normal file
31
app/src/main/res/drawable/ic_day_night_mode.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<!-- Sun (Top-Left Position) -->
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6.5,6.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0" />
|
||||
|
||||
<!-- Sun Rays (Using strokes to ensure visibility) -->
|
||||
<path
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="1.2"
|
||||
android:strokeLineCap="round"
|
||||
android:pathData="M6.5,1.5v1.5 M6.5,11.5v-1.5 M1.5,6.5h1.5 M11.5,6.5h-1.5 M3,3l1,1 M10,10l-1,-1 M3,10l1,-1 M10,3l-1,1" />
|
||||
|
||||
<!-- Diagonal Separator (Top-Right to Bottom-Left) -->
|
||||
<path
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="1.2"
|
||||
android:strokeLineCap="round"
|
||||
android:pathData="M18.5,5.5 L5.5,18.5" />
|
||||
|
||||
<!-- Moon Crescent (Bottom-Right Position) -->
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17.5,12.5c-2.48,0 -4.5,2.02 -4.5,4.5s2.02,4.5 4.5,4.5s4.5,-2.02 4.5,-4.5c0,-0.23 -0.02,-0.46 -0.05,-0.68c-0.49,0.68 -1.29,1.13 -2.2,1.13c-1.49,0 -2.7,-1.21 -2.7,-2.7c0,-0.91 0.45,-1.71 1.13,-2.2c-0.22,-0.03 -0.45,-0.05 -0.68,-0.05z" />
|
||||
|
||||
</vector>
|
||||
21
app/src/main/res/drawable/ic_standard_game_mode.xml
Normal file
21
app/src/main/res/drawable/ic_standard_game_mode.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- Stylized Dartboard Section for Game Modes -->
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8S16.41,20 12,20z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,4.5c-4.14,0 -7.5,3.36 -7.5,7.5s3.36,7.5 7.5,7.5s7.5,-3.36 7.5,-7.5S16.14,4.5 12,4.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5s5,2.24 5,5S14.76,17 12,17z" />
|
||||
<!-- Center Point (Bullseye) -->
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,10.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5S12.83,10.5 12,10.5z" />
|
||||
<!-- Radial Lines (Segments) -->
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,4V2M12,22v-2M4,12H2m20,0h-2M6.34,6.34L4.93,4.93m14.14,14.14l-1.41,-1.41M6.34,17.66l-1.41,1.41M19.07,4.93l-1.41,1.41" />
|
||||
</vector>
|
||||
@@ -3,7 +3,8 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/midnight_black">
|
||||
android:background="@color/surface_primary"
|
||||
android:id="@+id/main">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btnDeletePlayer"
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background_primary">
|
||||
tools:context=".GameActivity"
|
||||
android:background="@color/background_primary"
|
||||
android:id="@+id/main">
|
||||
|
||||
<!-- 1. HIGH-IMPACT SCOREBOARD (TOP) -->
|
||||
<LinearLayout
|
||||
|
||||
10
app/src/main/res/layout/settings_activity.xml
Normal file
10
app/src/main/res/layout/settings_activity.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/main">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
||||
19
app/src/main/res/values/arrays.xml
Normal file
19
app/src/main/res/values/arrays.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Labels of the standard game mode preference -->
|
||||
<string-array name="pref_standard_game_mode_labels">
|
||||
<item>701</item>
|
||||
<item>501</item>
|
||||
<item>301</item>
|
||||
<item>Cricket</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Values of the standard game mode preference -->
|
||||
<string-array name="pref_standard_game_mode_values">
|
||||
<item>@string/pref_game_mode_701_value</item>
|
||||
<item>@string/pref_game_mode_501_value</item>
|
||||
<item>@string/pref_game_mode_301_value</item>
|
||||
<item>@string/pref_game_mode_cricket_value</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -29,8 +29,36 @@
|
||||
<string name="txt_game_btn_bull">Bull</string>
|
||||
<string name="txt_game_btn_submit">Submit Turn</string>
|
||||
|
||||
<!-- Preference Strings -->
|
||||
<string name="pref_key_day_night_mode">day_night_mode</string>
|
||||
<string name="pref_key_standard_game_mode">standard_game_mode</string>
|
||||
<string name="pref_game_mode_701_value">701</string>
|
||||
<string name="pref_game_mode_501_value">501</string>
|
||||
<string name="pref_game_mode_301_value">301</string>
|
||||
<string name="pref_game_mode_cricket_value">Cricket</string>
|
||||
<string name="pref_desc_day_night_mode">Day/Night Mode</string>
|
||||
<string name="pref_title_standard_game_mode">Standard Game Mode</string>
|
||||
<string name="pref_desc_standard_game_mode">The Standard Game Mode to be selected for the Quick Start\nCurrently selected: %s</string>
|
||||
|
||||
|
||||
<!-- Image Content description -->
|
||||
<string name="cd_txt_oche_logo">Application Logo</string>
|
||||
<string name="cd_txt_settings_button">Settings</string>
|
||||
<string name="cd_text_historic_record">Match History</string>
|
||||
<string name="title_activity_settings">SettingsActivity</string>
|
||||
|
||||
<!-- Preference Titles -->
|
||||
<string name="messages_header">Messages</string>
|
||||
<string name="sync_header">Sync</string>
|
||||
|
||||
<!-- Messages Preferences -->
|
||||
<string name="signature_title">Your signature</string>
|
||||
<string name="reply_title">Default reply action</string>
|
||||
|
||||
<!-- Sync Preferences -->
|
||||
<string name="sync_title">Sync email periodically</string>
|
||||
<string name="attachment_title">Download incoming attachments</string>
|
||||
<string name="attachment_summary_on">Automatically download attachments for incoming emails
|
||||
</string>
|
||||
<string name="attachment_summary_off">Only download attachments when manually requested</string>
|
||||
</resources>
|
||||
19
app/src/main/res/xml/main_menu_preferences.xml
Normal file
19
app/src/main/res/xml/main_menu_preferences.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<SwitchPreference
|
||||
app:key="@string/pref_key_day_night_mode"
|
||||
app:title="@string/pref_desc_day_night_mode"
|
||||
android:icon="@drawable/ic_day_night_mode"/>
|
||||
|
||||
<ListPreference
|
||||
app:key="@string/pref_key_standard_game_mode"
|
||||
app:title="@string/pref_title_standard_game_mode"
|
||||
app:summary="@string/pref_desc_standard_game_mode"
|
||||
android:icon="@drawable/ic_standard_game_mode"
|
||||
android:defaultValue="@string/pref_game_mode_501_value"
|
||||
android:entries="@array/pref_standard_game_mode_labels"
|
||||
android:entryValues="@array/pref_standard_game_mode_values" />
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -9,6 +9,8 @@ activity = "1.12.2"
|
||||
constraintlayout = "2.2.1"
|
||||
glide = "5.0.5"
|
||||
room = "2.8.4"
|
||||
preferences = "1.2.0"
|
||||
preference = "1.2.1"
|
||||
|
||||
[libraries]
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
@@ -21,6 +23,8 @@ constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayo
|
||||
glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide"}
|
||||
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room"}
|
||||
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room"}
|
||||
preferences = { group = "androidx.preference", name="preference-ktx", version.ref="preferences" }
|
||||
preference = { group = "androidx.preference", name = "preference", version.ref = "preference" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
Reference in New Issue
Block a user