Skip to content
56 changes: 21 additions & 35 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,39 +1,25 @@
name: Contracts CI
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
name: Build and test Dojo contracts

on: [push, pull_request]

jobs:
build-and-test:
sozo-test:
runs-on: ubuntu-latest
env:
DOJO_VERSION: v1.2.2
steps:
- name: Install Rust
uses: actions-rs/toolchain@v1
- uses: actions/checkout@v3
- uses: software-mansion/setup-scarb@v1
with:
toolchain: stable
override: true

- name: Checkout code
uses: actions/checkout@v3

- name: Install asdf
uses: asdf-vm/actions/setup@v2

- name: Install plugins
run: |
asdf plugin add scarb
asdf install scarb 2.9.4
asdf global scarb 2.9.4
asdf plugin add dojo https://github.com/dojoengine/asdf-dojo
asdf install dojo 1.4.0
asdf global dojo 1.4.0
- name: Build contracts
run: |
sozo build
- name: Run tests
run: |
sozo test
- name: Check formatting
run: |
scarb fmt --check
scarb-version: "2.9.2"
- run: |
curl -L https://install.dojoengine.org | bash
/home/runner/.config/.dojo/bin/dojoup -v ${{ env.DOJO_VERSION }}
- run: |
/home/runner/.config/.dojo/bin/sozo build
/home/runner/.config/.dojo/bin/sozo test
if [[ `git status --porcelain` ]]; then
echo The git repo is dirty
echo "Make sure to run \"sozo build\" after changing Scarb.toml"
exit 1
fi
20 changes: 20 additions & 0 deletions src/model/game_model.cairo
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
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
194 changes: 192 additions & 2 deletions src/systems/Snooknet.cairo
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
use dojo_starter::interfaces::ISnooknet::ISnooknet;
use dojo_starter::model::game_model::{Game, GameTrait, GameState, GameCounter};
use dojo_starter::model::game_model::{Game, GameTrait, GameState, GameCounter, PlayerRating};

// dojo decorator
#[dojo::contract]
pub mod Snooknet {
use super::{ISnooknet, Game, GameTrait, GameCounter, GameState};
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;


#[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 @@ -56,6 +65,10 @@ 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 @@ -83,11 +96,49 @@ 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 All @@ -108,6 +159,145 @@ pub mod Snooknet {
fn world_default(self: @ContractState) -> dojo::world::WorldStorage {
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
}
}
}
}

Loading
Loading