Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Mainnet] Hashbattle Gaming Contract #84

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
31 changes: 31 additions & 0 deletions Mainnet/HashBattle/HashBattle.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31624.102
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HashBattle", "HashBattle\HashBattle.csproj", "{D711FA52-750E-481B-9BC5-2E07EBF58240}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HashBattleTest", "HashBattleTest\HashBattleTest.csproj", "{2A7F2670-A17F-46E6-BF35-A2C55BC7DCB1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D711FA52-750E-481B-9BC5-2E07EBF58240}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D711FA52-750E-481B-9BC5-2E07EBF58240}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D711FA52-750E-481B-9BC5-2E07EBF58240}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D711FA52-750E-481B-9BC5-2E07EBF58240}.Release|Any CPU.Build.0 = Release|Any CPU
{2A7F2670-A17F-46E6-BF35-A2C55BC7DCB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2A7F2670-A17F-46E6-BF35-A2C55BC7DCB1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2A7F2670-A17F-46E6-BF35-A2C55BC7DCB1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A7F2670-A17F-46E6-BF35-A2C55BC7DCB1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {88A96B52-6F00-40C9-AA28-C3D79E3BC0DF}
EndGlobalSection
EndGlobal
299 changes: 299 additions & 0 deletions Mainnet/HashBattle/HashBattle/Arena.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
using Stratis.SmartContracts;
using System;
using System.Text;

/// <summary>
/// A Stratis smart contract for running a game battle where owner will start the battle and maximum 4 users can enter a battle
/// </summary>
public class Arena : SmartContract
{
private void SetBattle(ulong battleId, BattleMain battle)
{
State.SetStruct($"battle:{battleId}", battle);
}
public BattleMain GetBattle(ulong battleId)
{
return State.GetStruct<BattleMain>($"battle:{battleId}");
}
private void SetUser(ulong battleId, Address address, BattleUser user)
{
State.SetStruct($"user:{battleId}:{address}", user);
}
public BattleUser GetUser(ulong battleId, Address address)
{
return State.GetStruct<BattleUser>($"user:{battleId}:{address}");
}
private void SetHighestScorer(ulong battleId, BattleHighestScorer highestScorer)
{
State.SetStruct($"scorer:{battleId}", highestScorer);
}
public BattleHighestScorer GetHighestScorer(ulong battleId)
{
return State.GetStruct<BattleHighestScorer>($"scorer:{battleId}");
}
private void SetUserIndex(ulong battleId, uint userindex)
{
State.SetUInt32($"user:{battleId}", userindex);
}
private uint GetUserIndex(ulong battleId)
{
return State.GetUInt32($"user:{battleId}");
}
private void SetScoreSubmittedCount(ulong battleId, uint scoresubmitcount)
{
State.SetUInt32($"scoresubmit:{battleId}", scoresubmitcount);
}
private uint GetScoreSubmittedCount(ulong battleId)
{
return State.GetUInt32($"scoresubmit:{battleId}");
}
/// <summary>
/// Set the address deploying the contract as battle owner
/// </summary>
public Address Owner
{
get => State.GetAddress(nameof(Owner));
private set => State.SetAddress(nameof(Owner), value);
}
public Address PendingOwner
{
get => State.GetAddress(nameof(PendingOwner));
private set => State.SetAddress(nameof(PendingOwner), value);
}
public uint MaxUsers
{
get => State.GetUInt32(nameof(MaxUsers));
private set => State.SetUInt32(nameof(MaxUsers), value);
}
/// <summary>
/// Set the unique battleId of each battle
/// </summary>
public ulong NextBattleId
{
get => State.GetUInt64(nameof(NextBattleId));
private set => State.SetUInt64(nameof(NextBattleId), value);
}

public Arena(ISmartContractState smartContractState, uint maxUsers) : base(smartContractState)
{
Owner = Message.Sender;
MaxUsers = maxUsers;
NextBattleId = 1;
}

/// <summary>
/// Only owner can set new owner and new owner will be in pending state
/// till new owner will call <see cref="ClaimOwnership"></see> method.
/// </summary>
/// <param name="newOwner">The new owner which is going to be in pending state</param>
public void SetPendingOwner(Address newOwner)
{
EnsureOwnerOnly();
PendingOwner = newOwner;

Log(new OwnershipTransferRequestedLog { CurrentOwner = Owner, PendingOwner = newOwner });
}

/// <summary>
/// Waiting be called after new owner is requested by <see cref="SetPendingOwner"/> call.
/// Pending owner will be new owner after successfull call.
/// </summary>
public void ClaimOwnership()
{
var newOwner = PendingOwner;

Assert(newOwner == Message.Sender, "ClaimOwnership must be called by the new(pending) owner.");

var oldOwner = Owner;
Owner = newOwner;
PendingOwner = Address.Zero;

Log(new OwnershipTransferedLog { PreviousOwner = oldOwner, NewOwner = newOwner });
}
/// <summary>
/// Battle owner will start the battle
/// </summary>
public ulong StartBattle(ulong fee)
{
Assert(Message.Sender == Owner, "Only battle owner can start game.");
Assert(fee < ulong.MaxValue / MaxUsers, "Fee is too high");

var battleId = NextBattleId;
NextBattleId += 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You access value of NextBattleId twice in this code so please follow my recommandation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or alternatively you can use below code.

Suggested change
NextBattleId += 1;
NextBattleId = battleId + 1;


var battle = new BattleMain
{
BattleId = battleId,
Fee = fee,
Users = new Address[MaxUsers]
};
SetBattle(battleId, battle);

Log(new BattleStartedLog { BattleId = battleId, Address = Message.Sender });
return battleId;
}
/// <summary>
/// 4 different user will enter the battle
/// </summary>
public void EnterBattle(ulong battleId)
{
var battle = GetBattle(battleId);

Assert(battle.Winner == Address.Zero, "Battle not found.");

Assert(battle.Fee == Message.Value, "Battle fee is not matching with entry fee paid.");

var user = GetUser(battleId, Message.Sender);

Assert(!user.ScoreSubmitted, "The user already submitted score.");

SetUser(battleId, Message.Sender, user);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are sending user address 2 times in here. One is Message.Sender and other is as user parameter with Address property. I question it is needed to add Address property for user ? Because you can not get/set user without knowing the address already.


var userindex = GetUserIndex(battleId);
Assert(userindex != MaxUsers, "Max user reached for this battle.");
battle.Users.SetValue(Message.Sender, userindex);
SetUserIndex(battleId, userindex + 1);

SetBattle(battleId, battle);

Log(new BattleEnteredLog { BattleId = battleId, Address = Message.Sender });
}
/// <summary>
/// 4 different user will end the battle and submit the score
/// </summary>
public void EndBattle(Address userAddress, ulong battleId, uint score)
{
Assert(Message.Sender == Owner, "Only battle owner can end game.");

var ScoreSubmittedCount = GetScoreSubmittedCount(battleId);
Assert(ScoreSubmittedCount < MaxUsers, "All users already submitted score.");

var battle = GetBattle(battleId);

Assert(battle.Winner == Address.Zero, "Battle not found.");

var user = GetUser(battleId, userAddress);

Assert(!user.ScoreSubmitted, "The user already submitted score.");

user.ScoreSubmitted = true;

SetUser(battleId, userAddress, user);

ScoreSubmittedCount += 1;
SetScoreSubmittedCount(battleId, ScoreSubmittedCount);

var highestScorer = GetHighestScorer(battleId);

if (score > highestScorer.Score)
{
highestScorer.Score = score;
highestScorer.HighestScorer = userAddress;
highestScorer.HighestScoreCount = 1;

SetHighestScorer(battleId, highestScorer);
}
else if (score == highestScorer.Score)
{
highestScorer.HighestScoreCount++;
SetHighestScorer(battleId, highestScorer);
}

if (ScoreSubmittedCount == MaxUsers)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have to assert ScoreSubmittedCount > MaxUsers case at the beginning of the method ?

{
highestScorer = GetHighestScorer(battleId);
if (highestScorer.HighestScoreCount > 1)
CancelBattle(battle);
else
ProcessWinner(battle, highestScorer.HighestScorer);
}

Log(new BattleEndedLog { BattleId = battleId, Address = Message.Sender });
}
/// <summary>
/// Get winner address
/// </summary>
public Address GetWinner(ulong battleId)
{
var battle = GetBattle(battleId);
return battle.Winner;
}
/// <summary>
/// Process winner when all user scores are submitted
/// </summary>
private void ProcessWinner(BattleMain battle, Address winnerAddress)
{
battle.Winner = winnerAddress;
SetBattle(battle.BattleId, battle);
ProcessPrize(battle);
}
/// <summary>
/// Send 3/4 amount to winner and 1/4 amount to battle owner
/// </summary>
private void ProcessPrize(BattleMain battle)
{
var prize = battle.Fee * (MaxUsers - 1);
Transfer(battle.Winner, prize);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If winner address is a contract address then this transfer has possibility to fail. So you can use withdrawal pattern(pull) in here. It will transfer funds immediately if destination is not a contract address and if it is then it will fallback to withdrawal pattern.

private bool SafeTransfer(Address to, ulong amount)

private bool SafeTransfer(Address to, ulong amount)
{
    if (State.IsContract(to))
    {
        var balance = GetBalance(to) + amount;

        SetBalance(to, balance);

        return true;
    }

    var result = Transfer(to, amount);

    return result.Success;
}

public bool Withdraw()
{
    EnsureNotPayable();

    var amount = GetBalance(Message.Sender);

    Assert(amount > 0);

    SetBalance(Message.Sender, 0);

    var transfer = Transfer(Message.Sender, amount);

    Assert(transfer.Success, "Transfer failed.");

    Log(new BalanceRefundedLog { To = Message.Sender, Amount = amount });

    return transfer.Success;
}

public ulong GetBalance(Address address)
{
    return State.GetUInt64($"Balance:{address}");
}

private void SetBalance(Address address, ulong balance)
{
    State.SetUInt64($"Balance:{address}", balance);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Winner address will not be a contract address. 4 players will play the game with their wallet address.

Copy link
Member

@YakupIpek YakupIpek Jun 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how do you ensure that user addresses will not belong to a another contract ?

Transfer(Owner, battle.Fee);
}
/// <summary>
/// Cancel battle and refund the fee amount
/// </summary>
private void CancelBattle(BattleMain battle)
{
battle.IsCancelled = true;
SetBattle(battle.BattleId, battle);

Transfer(battle.Users[0], battle.Fee);
Transfer(battle.Users[1], battle.Fee);
Transfer(battle.Users[2], battle.Fee);
Transfer(battle.Users[3], battle.Fee);
}
private void EnsureOwnerOnly()
{
Assert(Message.Sender == Owner, "The method is owner only.");
}
public struct BattleMain
{
public ulong BattleId;
public Address Winner;
public Address[] Users;
public ulong Fee;
public bool IsCancelled;
}
public struct BattleUser
{
public bool ScoreSubmitted;
}
public struct BattleHighestScorer
{
public uint Score;
public uint HighestScoreCount;
public Address HighestScorer;
}
public struct OwnershipTransferedLog
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo

Suggested change
public struct OwnershipTransferedLog
public struct OwnershipTransferredLog

{
[Index] public Address PreviousOwner;
[Index] public Address NewOwner;
}
public struct OwnershipTransferRequestedLog
{
[Index] public Address CurrentOwner;
[Index] public Address PendingOwner;
}
public struct BattleStartedLog
{
[Index] public ulong BattleId;
[Index] public Address Address;
}
public struct BattleEnteredLog
{
[Index] public ulong BattleId;
[Index] public Address Address;
}
public struct BattleEndedLog
{
[Index] public ulong BattleId;
[Index] public Address Address;
}
}
11 changes: 11 additions & 0 deletions Mainnet/HashBattle/HashBattle/HashBattle.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>

<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Stratis.SmartContracts" Version="2.0.0" />
</ItemGroup>
</Project>
Loading