Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ jobs:
sozo test
- name: Check formatting
run: |
scarb fmt --check
scarb fmt --check
20 changes: 0 additions & 20 deletions src/model/game_model.cairo
Original file line number Diff line number Diff line change
@@ -1,26 +1,6 @@
use starknet::{ContractAddress, contract_address_const};
// Keeps track of the state of the game

// New PlayerRating model to store player ratings
#[derive(Copy, Drop, Serde, Introspect)]
#[dojo::model]
pub struct PlayerRating {
#[key]
pub player: ContractAddress,
pub rating: u32 // Elo rating
}

// Trait for PlayerRating operations
pub trait PlayerRatingTrait {
fn new(player: ContractAddress, rating: u32) -> PlayerRating;
}

impl PlayerRatingImpl of PlayerRatingTrait {
fn new(player: ContractAddress, rating: u32) -> PlayerRating {
PlayerRating { player, rating }
}
}

#[derive(Serde, Copy, Drop, Introspect, PartialEq)]
#[dojo::model]
pub struct GameCounter {
Expand Down
199 changes: 1 addition & 198 deletions src/systems/Snooknet.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use dojo::event::EventStorage;


use dojo_starter::interfaces::ISnooknet::ISnooknet;
use dojo_starter::model::game_model::{Game, GameTrait, GameState, GameCounter, PlayerRating};
use dojo_starter::model::game_model::{Game, GameTrait, GameState, GameCounter};
use dojo_starter::model::tournament_model::{
Tournament as TournamentModel, TournamentTrait, TournamentStatus, TournamentReward,
TournamentCounter,
Expand All @@ -16,12 +16,6 @@ use dojo_starter::model::player_model::{Player, PlayerTrait};
// dojo decorator
#[dojo::contract]
pub mod Snooknet {
use super::{ISnooknet, Game, GameTrait, GameCounter, GameState, PlayerRating};
use starknet::{
ContractAddress, get_caller_address, get_block_timestamp, contract_address_const,
};
use dojo::model::{ModelStorage};
use dojo::event::EventStorage;
use super::*;

#[derive(Copy, Drop, Serde)]
Expand All @@ -32,15 +26,6 @@ pub mod Snooknet {
pub timestamp: u64,
}

#[derive(Copy, Drop, Serde)]
#[dojo::event]
pub struct RatingUpdated {
#[key]
pub player: ContractAddress,
pub new_rating: u32,
}


#[derive(Copy, Drop, Serde)]
#[dojo::event]
pub struct GameCreated {
Expand Down Expand Up @@ -125,10 +110,6 @@ pub mod Snooknet {
let player_1 = get_caller_address();
let player_2 = opponent;

// Initialize ratings for players if they don't exist
self.ensure_player_rating(player_1);
self.ensure_player_rating(player_2);

// Create a new game
let mut new_game: Game = GameTrait::new(
game_id,
Expand Down Expand Up @@ -156,49 +137,11 @@ pub mod Snooknet {
let caller = get_caller_address();

let timestamp = get_block_timestamp();

assert((caller == game.player1) || (caller == game.player2), 'Not a Player');

assert(
(winner == game.player1)
|| (winner == game.player2)
|| (winner == contract_address_const::<0x0>()),
'Invalid winner',
);

// Ensure game is not already finished
assert(game.state != GameState::Finished, 'Game already ended');

game.winner = winner;
game.state = GameState::Finished;
game.updated_at = get_block_timestamp();

// Update player ratings using Elo algorithm
if winner != contract_address_const::<0x0>() {
// Not a draw
let (new_rating1, new_rating2) = self
.elo_function(game.player1, game.player2, winner);
let mut rating1: PlayerRating = world.read_model(game.player1);
let mut rating2: PlayerRating = world.read_model(game.player2);
rating1.rating = new_rating1;
rating2.rating = new_rating2;
world.write_model(@rating1);
world.write_model(@rating2);
world.emit_event(@RatingUpdated { player: game.player1, new_rating: new_rating1 });
world.emit_event(@RatingUpdated { player: game.player2, new_rating: new_rating2 });
} else {
// Draw: both players get 0.5 score
let (new_rating1, new_rating2) = self.elo_function_draw(game.player1, game.player2);
let mut rating1: PlayerRating = world.read_model(game.player1);
let mut rating2: PlayerRating = world.read_model(game.player2);
rating1.rating = new_rating1;
rating2.rating = new_rating2;
world.write_model(@rating1);
world.write_model(@rating2);
world.emit_event(@RatingUpdated { player: game.player1, new_rating: new_rating1 });
world.emit_event(@RatingUpdated { player: game.player2, new_rating: new_rating2 });
}

world.write_model(@game);
world.emit_event(@Winner { game_id, winner });
world.emit_event(@GameEnded { game_id, timestamp });
Expand Down Expand Up @@ -299,145 +242,6 @@ pub mod Snooknet {
self.world(@"Snooknet")
}

fn ensure_player_rating(ref self: ContractState, player: ContractAddress) {
let mut world = self.world_default();
let mut rating: PlayerRating = world.read_model(player);
if rating.rating == 0 {
rating.rating = 1500;
world.write_model(@rating);
}
}

// Elo function for win/loss
fn elo_function(
ref self: ContractState,
player1: ContractAddress,
player2: ContractAddress,
winner: ContractAddress,
) -> (u32, u32) {
let mut world = self.world_default();
let rating1: PlayerRating = world.read_model(player1);
let rating2: PlayerRating = world.read_model(player2);
let r1 = rating1.rating; // u32
let r2 = rating2.rating; // u32
let k = 32_u32; // Elo K-factor

// Calculate expected scores (scaled 0 to 1000)
let expected1 = self.calculate_expected(r1, r2); // u32
let expected2 = self.calculate_expected(r2, r1); // u32

// Assign scores based on winner
let (score1, score2) = if winner == player1 {
(1000_u32, 0_u32)
} else {
(0_u32, 1000_u32)
};

// Calculate rating changes safely
let new_rating1 = if score1 >= expected1 {
// Positive or zero change (e.g., winner)
let delta = (score1 - expected1) * k / 1000;
r1 + delta
} else {
// Negative change (e.g., loser)
let delta = self.safe_subtract(expected1, score1) * k / 1000;
let min_rating = 100_u32; // Minimum rating to prevent too-low values
if r1 <= delta {
min_rating
} else {
r1 - delta
}
};

let new_rating2 = if score2 >= expected2 {
// Positive or zero change
let delta = (score2 - expected2) * k / 1000;
r2 + delta
} else {
// Negative change
let delta = self.safe_subtract(expected2, score2) * k / 1000;
let min_rating = 100_u32;
if r2 <= delta {
min_rating
} else {
r2 - delta
}
};

(new_rating1, new_rating2)
}

// Elo function for draw
fn elo_function_draw(
ref self: ContractState, player1: ContractAddress, player2: ContractAddress,
) -> (u32, u32) {
let mut world = self.world_default();
let rating1: PlayerRating = world.read_model(player1);
let rating2: PlayerRating = world.read_model(player2);
let r1 = rating1.rating;
let r2 = rating2.rating;
let k = 32_u32;

let expected1 = self.calculate_expected(r1, r2);
let expected2 = self.calculate_expected(r2, r1);
let score = 500_u32; // Draw score (scaled)

let new_rating1 = if score >= expected1 {
let delta = (score - expected1) * k / 1000;
r1 + delta
} else {
let delta = self.safe_subtract(expected1, score) * k / 1000;
let min_rating = 100_u32;
if r1 <= delta {
min_rating
} else {
r1 - delta
}
};

let new_rating2 = if score >= expected2 {
let delta = (score - expected2) * k / 1000;
r2 + delta
} else {
let delta = self.safe_subtract(expected2, score) * k / 1000;
let min_rating = 100_u32;
if r2 <= delta {
min_rating
} else {
r2 - delta
}
};

(new_rating1, new_rating2)
}

// Helper function to calculate expected score
fn calculate_expected(self: @ContractState, r1: u32, r2: u32) -> u32 {
if r1 > r2 {
let diff = r1 - r2;
if diff > 400 {
1000_u32
} else {
500_u32 + (diff * 5) / 4
}
} else {
let diff = r2 - r1;
if diff > 400 {
0_u32
} else {
500_u32 - (diff * 5) / 4
}
}
}

fn safe_subtract(self: @ContractState, a: u32, b: u32) -> u32 {
if a >= b {
a - b
} else {
0_u32
}
}

fn create_new_tournament_id(ref self: ContractState) -> u256 {
let mut world = self.world_default();
let mut tournament_counter: TournamentCounter = world.read_model('v0');
Expand All @@ -449,4 +253,3 @@ pub mod Snooknet {
}
}


Loading
Loading