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 3 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
271 changes: 271 additions & 0 deletions Mainnet/HashBattle/HashBattle/Arena.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
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 const uint MaxUsers = 4;
Copy link
Member

Choose a reason for hiding this comment

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

If there is possibility in future that MaxUsers can be higher than 4 than you can make it constructor parameter.

public struct BattleMain
Copy link
Member

Choose a reason for hiding this comment

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

All struct definitions should go to bottom of the class.

{
public ulong BattleId;
public Address Winner;
public Address[] Users;
public ulong Fee;
public bool IsCancelled;
}
public struct BattleUser
{
public uint Score;
public bool ScoreSubmitted;
}
public struct BattleHighestScorer
{
public uint Score;
public Address[] Scorers;
}
public struct ClaimPendingDeployerOwnershipLog
Copy link
Member

Choose a reason for hiding this comment

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

This log type name should be not changed. It should stays as in original.

{
[Index] public Address From;
[Index] public Address To;
}
public struct SetPendingDeployerOwnershipLog
Copy link
Member

Choose a reason for hiding this comment

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

This log type name should be not changed. It should stays as in original.

{
[Index] public Address From;
[Index] public Address To;
}
public struct BattleEventLog
{
[Index] public string Event;
[Index] public ulong BattleId;
[Index] public Address Address;
}
/// <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);
}
/// <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) : base(smartContractState)
{
Owner = Message.Sender;
NextBattleId = 1;
}
public void SetPendingOwnership(Address pendingOwner)
{
EnsureOwnerOnly();

PendingOwner = pendingOwner;

Log(new SetPendingDeployerOwnershipLog { From = Message.Sender, To = pendingOwner });
}
public void ClaimPendingOwnership()
{
var pendingOwner = PendingOwner;

Assert(Message.Sender == pendingOwner, "HASHBATTLE: UNAUTHORIZED");

var oldOwner = Owner;

Owner = pendingOwner;
PendingOwner = Address.Zero;

Log(new ClaimPendingDeployerOwnershipLog { From = oldOwner, To = pendingOwner });
}
/// <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 BattleEventLog { Event = "Start", 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));
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
SetUserIndex(battleId, (userindex + 1));
SetUserIndex(battleId, userindex + 1);


SetBattle(battleId, battle);

Log(new BattleEventLog { Event = "Enter", BattleId = battleId, Address = Message.Sender });
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Log(new BattleEventLog { Event = "Enter", BattleId = battleId, Address = Message.Sender });
Log(new BattleStartedLog { 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 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.Score = score;
Copy link
Member

Choose a reason for hiding this comment

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

What for we save Score on user ? It seems there is no use case for it.

user.ScoreSubmitted = true;

SetUser(battleId, userAddress, user);

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

var highestScorer = GetHighestScorer(battleId);

if (score > highestScorer.Score)
SetHighestScorer(battleId, new BattleHighestScorer { Scorers = new Address[] { userAddress }, Score = score });
else if (score == highestScorer.Score)
{
var scorers = highestScorer.Scorers;
Array.Resize(ref scorers, scorers.Length + 1);
Copy link
Member

Choose a reason for hiding this comment

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

This allocate new array actually and iterate it by copying fully. We should avoid it. What you need to is highest score and how many times same highest score counted and highest user. You can keep last highest one always whenever equal score submited. So you can do something like this

    public struct BattleHighestScorer
    {
        public uint Score;
        public uint HighestScoreCount;
        public Address HighestScorer;
    }
      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)
        {
            highestScorer = GetHighestScorer(battleId);
            if (highestScorer.HighestScoreCount > 1)
                CancelBattle(battle);
            else
                ProcessWinner(battle, highestScorer.HighestScorer);
        }

scorers[scorers.Length - 1] = userAddress;
SetHighestScorer(battleId, new BattleHighestScorer { Scorers = scorers, Score = score });
}

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.Scorers.Length > 1)
CancelBattle(battle);
else
ProcessWinner(battle, highestScorer.Scorers[0]);
}

Log(new BattleEventLog { Event = "End", BattleId = battleId, Address = Message.Sender });
Copy link
Member

Choose a reason for hiding this comment

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

It is usually recommended way to log by type name like BattleEndedLog instead of flag property.

}
/// <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 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);
Copy link
Member

Choose a reason for hiding this comment

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

Always use : for multiple key seperator and do this for others.

Suggested change
State.SetStruct($"user:{battleId}-{address}", user);
State.SetStruct($"user:{battleId}:{address}", user);

Copy link
Member

Choose a reason for hiding this comment

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

Also these are getters and setters method over State so please move them to top of constructor.

}
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}");
}
private void EnsureOwnerOnly()
{
Assert(Message.Sender == Owner, "HASHBATTLE: UNAUTHORIZED");
}
}
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