feat: add send message to players with runtime.Notify
This commit is contained in:
parent
03977ecd64
commit
fb18553355
1 changed files with 468 additions and 0 deletions
468
room_contract.go
Normal file
468
room_contract.go
Normal file
|
@ -0,0 +1,468 @@
|
|||
package contracts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/std"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
||||
"sort"
|
||||
)
|
||||
|
||||
var notificationName string
|
||||
|
||||
// init initializes notificationName before calling any other smart-contract method
|
||||
func init() {
|
||||
notificationName = "Hello world!"
|
||||
}
|
||||
|
||||
// RuntimeNotify sends runtime notification with "Hello world!" name
|
||||
func RuntimeNotify(args []any) {
|
||||
runtime.Notify(notificationName, args)
|
||||
}
|
||||
|
||||
// CONSTANTS
|
||||
|
||||
const (
|
||||
RoomStatusWaiting = "waiting" // Waiting for the game to start, players are joining
|
||||
RoomStatusGaming = "gaming" // In this phase, players are ready, but the question hasn't been asked yet
|
||||
RoomStatusAnswering = "answering" // Phase when the round has started and the question has been asked, players can submit answers
|
||||
RoomStatusVoting = "voting" // Voting phase, where players select the best answer from the options
|
||||
RoomStatusFinished = "finished" // Game is finished, and results have been determined
|
||||
)
|
||||
|
||||
// STRUCTS
|
||||
|
||||
type Room struct {
|
||||
Id string
|
||||
Host interop.Hash160
|
||||
Status string
|
||||
PrizePool int
|
||||
RoundWinnersCount int
|
||||
GameWinnersCount int
|
||||
Players []Player
|
||||
Rounds []Round
|
||||
}
|
||||
|
||||
type Round struct {
|
||||
Question string
|
||||
Answers []Answer
|
||||
}
|
||||
|
||||
type Answer struct {
|
||||
Wallet interop.Hash160
|
||||
Content string
|
||||
Votes []interop.Hash160 // Wallets who voted for answer
|
||||
}
|
||||
|
||||
type Player struct {
|
||||
Wallet interop.Hash160
|
||||
RoundsWon int
|
||||
IsReady bool
|
||||
IsVotedToFinish bool
|
||||
}
|
||||
|
||||
// GLOBAL PRIVATE METHODS FOR ROOM
|
||||
|
||||
func getSender() interop.Hash160 {
|
||||
return runtime.GetScriptContainer().Sender
|
||||
}
|
||||
|
||||
// Function to set room in storage with serialize
|
||||
func setRoom(ctx storage.Context, room *Room) {
|
||||
var serializedRoom = std.Serialize(room)
|
||||
storage.Put(ctx, "room:"+room.Id, serializedRoom)
|
||||
}
|
||||
|
||||
// Function to get room from storage with deserialize
|
||||
func getRoom(ctx storage.Context, roomId string) Room {
|
||||
var roomData = storage.Get(ctx, "room:"+roomId)
|
||||
|
||||
if roomData == nil {
|
||||
panic(fmt.Sprintf("Room with roomId=%s not found", roomId))
|
||||
}
|
||||
|
||||
var room = std.Deserialize(roomData.([]byte)).(Room)
|
||||
return room
|
||||
}
|
||||
|
||||
// Function to send message to players, event is recorded in blockchain
|
||||
// Could be read through getapplicationlog or RPC call
|
||||
func sendMessageToPlayers(notificationName, message string) {
|
||||
runtime.Notify(notificationName, message)
|
||||
}
|
||||
|
||||
// MAIN METHODS TO PLAY IN GAME
|
||||
|
||||
func CreateRoom(host interop.Hash160, RoundWinnersCount, GameWinnersCount int) string {
|
||||
var ctx = storage.GetContext()
|
||||
var id = uuid.NewString()
|
||||
var room = Room{
|
||||
Id: id,
|
||||
Host: host,
|
||||
Status: RoomStatusWaiting,
|
||||
PrizePool: 0,
|
||||
RoundWinnersCount: RoundWinnersCount,
|
||||
GameWinnersCount: GameWinnersCount,
|
||||
Players: []Player{},
|
||||
Rounds: []Round{},
|
||||
}
|
||||
|
||||
// todo: Добавить списание токенов за создание комнаты
|
||||
|
||||
setRoom(ctx, &room)
|
||||
return id
|
||||
}
|
||||
|
||||
func JoinRoom(roomId string) bool {
|
||||
var ctx = storage.GetContext()
|
||||
var room = getRoom(ctx, roomId)
|
||||
var wallet = getSender()
|
||||
|
||||
if room.Host.Equals(wallet) || room.Status != RoomStatusWaiting {
|
||||
return false // Host can not be player, player cannot join started room
|
||||
}
|
||||
|
||||
for _, player := range room.Players {
|
||||
if player.Wallet.Equals(wallet) {
|
||||
return false // Player already joined room
|
||||
}
|
||||
}
|
||||
|
||||
// todo: Добавить списание токенов за вход в комнату
|
||||
|
||||
var player = Player{
|
||||
Wallet: wallet,
|
||||
RoundsWon: 0,
|
||||
IsReady: false,
|
||||
IsVotedToFinish: false,
|
||||
}
|
||||
|
||||
room.Players = append(room.Players, player)
|
||||
setRoom(ctx, &room)
|
||||
return true
|
||||
}
|
||||
|
||||
func ConfirmReadiness(roomId string) bool {
|
||||
var ctx = storage.GetContext()
|
||||
var room = getRoom(ctx, roomId)
|
||||
var wallet = getSender()
|
||||
|
||||
for i, p := range room.Players {
|
||||
if p.Wallet.Equals(wallet) {
|
||||
if p.IsReady {
|
||||
return false // Player is already ready
|
||||
}
|
||||
room.Players[i].IsReady = true
|
||||
setRoom(ctx, &room)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false // Player not found in the room
|
||||
}
|
||||
|
||||
func StartGame(roomId string) bool {
|
||||
var ctx = storage.GetContext()
|
||||
var room = getRoom(ctx, roomId)
|
||||
|
||||
if !room.Host.Equals(getSender()) || room.Status != RoomStatusWaiting || len(room.Players) <= room.RoundWinnersCount {
|
||||
return false // Only host can start game, room status must be waiting and players count must be > count winners
|
||||
}
|
||||
|
||||
for _, player := range room.Players {
|
||||
if !player.IsReady {
|
||||
return false // If any player is not ready, the game can not be started
|
||||
}
|
||||
}
|
||||
|
||||
room.Status = RoomStatusGaming
|
||||
setRoom(ctx, &room)
|
||||
return true
|
||||
}
|
||||
|
||||
func AskQuestion(roomId string, question string) bool {
|
||||
var ctx = storage.GetContext()
|
||||
var room = getRoom(ctx, roomId)
|
||||
|
||||
if !room.Host.Equals(getSender()) || room.Status != RoomStatusGaming {
|
||||
return false // Only host can ask question, room status must be gaming
|
||||
}
|
||||
|
||||
var round = Round{
|
||||
Question: question,
|
||||
Answers: []Answer{},
|
||||
}
|
||||
room.Rounds = append(room.Rounds, round)
|
||||
room.Status = RoomStatusAnswering
|
||||
|
||||
sendMessageToPlayers("RoundQuestion", question)
|
||||
|
||||
// todo: Не забыть про nns
|
||||
// todo: Добавить списание токенов за создание вопроса
|
||||
|
||||
setRoom(ctx, &room)
|
||||
return true
|
||||
}
|
||||
|
||||
func roomContainsPlayer(players []Player, wallet interop.Hash160) bool {
|
||||
for _, player := range players {
|
||||
if player.Wallet.Equals(wallet) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func SendAnswer(roomId string, text string) bool {
|
||||
var ctx = storage.GetContext()
|
||||
var room = getRoom(ctx, roomId)
|
||||
var wallet = getSender()
|
||||
|
||||
if !roomContainsPlayer(room.Players, wallet) || room.Status != RoomStatusAnswering {
|
||||
return false // Only player can send content, room status must be answering
|
||||
}
|
||||
|
||||
// todo: Добавить списание токенов за добавление ответа
|
||||
|
||||
var round = room.Rounds[len(room.Rounds)-1]
|
||||
|
||||
for _, answer := range round.Answers {
|
||||
if answer.Wallet.Equals(wallet) {
|
||||
return false // Player cannot send answer twice
|
||||
}
|
||||
}
|
||||
|
||||
var answer = Answer{
|
||||
Wallet: wallet,
|
||||
Content: text,
|
||||
Votes: []interop.Hash160{},
|
||||
}
|
||||
|
||||
round.Answers = append(round.Answers, answer)
|
||||
room.Rounds[len(room.Rounds)-1] = round
|
||||
setRoom(ctx, &room)
|
||||
return true
|
||||
}
|
||||
|
||||
func EndQuestion(roomId string) bool {
|
||||
var ctx = storage.GetContext()
|
||||
var room = getRoom(ctx, roomId)
|
||||
|
||||
if !room.Host.Equals(getSender()) || room.Status != RoomStatusAnswering {
|
||||
return false // Only host can end question, room status must be answering
|
||||
}
|
||||
|
||||
room.Status = RoomStatusVoting
|
||||
|
||||
var round = room.Rounds[len(room.Rounds)-1]
|
||||
var result string
|
||||
for i, answer := range round.Answers {
|
||||
result += fmt.Sprintf("index:%d, player:%s, answer:%s\n", i, answer.Wallet, answer.Content)
|
||||
} // todo: нужно проверить нормально ли всё с \n в логах
|
||||
|
||||
sendMessageToPlayers("RoundAnswers", result)
|
||||
|
||||
setRoom(ctx, &room)
|
||||
return true
|
||||
}
|
||||
|
||||
func VoteAnswer(roomId string, answerIdx int) bool {
|
||||
var ctx = storage.GetContext()
|
||||
var room = getRoom(ctx, roomId)
|
||||
var wallet = getSender()
|
||||
|
||||
if room.Host.Equals(wallet) || room.Status != RoomStatusVoting {
|
||||
return false // Only player can choose answer, room status must be voting
|
||||
}
|
||||
|
||||
var round = room.Rounds[len(room.Rounds)-1]
|
||||
if !(0 <= answerIdx && answerIdx < len(round.Answers)) || round.Answers[answerIdx].Wallet.Equals(wallet) {
|
||||
return false // answerIdx is incorrect and player cannot vote for himself
|
||||
}
|
||||
|
||||
for _, votedWallet := range round.Answers[answerIdx].Votes {
|
||||
if votedWallet.Equals(wallet) {
|
||||
return false // Player cannot vote twice for one answer
|
||||
}
|
||||
}
|
||||
|
||||
round.Answers[answerIdx].Votes = append(round.Answers[answerIdx].Votes, wallet)
|
||||
setRoom(ctx, &room)
|
||||
return true
|
||||
}
|
||||
|
||||
func chooseWonAnswers(round Round, RoundWinnersCount int) []Answer {
|
||||
var wonAnswers []Answer
|
||||
var answers = round.Answers
|
||||
sort.Slice(answers, func(a, b int) bool {
|
||||
return len(answers[a].Votes) > len(answers[b].Votes)
|
||||
})
|
||||
|
||||
if RoundWinnersCount > len(round.Answers) {
|
||||
return round.Answers
|
||||
}
|
||||
|
||||
if RoundWinnersCount == 1 {
|
||||
return []Answer{answers[0]}
|
||||
}
|
||||
|
||||
// Choose wonAnswers from sorted answers. If the current answer has the same number of votes as the previous one,
|
||||
// we add it to the wonAnswers list. We increase the number of wonAnswers if there are multiple answers with the same
|
||||
// number of votes, as in cases where there are 5 answers with equal votes and RoundWinnersCount is 3, all should be included.
|
||||
var lastVote = len(answers[0].Votes)
|
||||
wonAnswers = append(wonAnswers, answers[0])
|
||||
for i := 1; i < len(answers) && len(wonAnswers) < RoundWinnersCount; i++ {
|
||||
var currentVote = len(answers[i].Votes)
|
||||
if lastVote == currentVote {
|
||||
RoundWinnersCount++ // todo: Могут возникнуть проблемы, надо протестировать
|
||||
}
|
||||
lastVote = currentVote
|
||||
wonAnswers = append(wonAnswers, answers[i])
|
||||
}
|
||||
|
||||
return wonAnswers
|
||||
}
|
||||
|
||||
// GetRoundWinner todo(-): Нам сказали, что ссылку на другой контракт нельзя чисто, используем nns
|
||||
// GetRoundWinner todo: Разные реализации распределения наград можно реализовать в контракте с деньгами
|
||||
func GetRoundWinner(roomId string) bool {
|
||||
var ctx = storage.GetContext()
|
||||
var room = getRoom(ctx, roomId)
|
||||
|
||||
if !room.Host.Equals(getSender()) || room.Status != RoomStatusVoting {
|
||||
return false // Only host can get winner, room status must be voting
|
||||
}
|
||||
|
||||
var round = room.Rounds[len(room.Rounds)-1]
|
||||
if len(round.Answers) == 0 {
|
||||
return false // Zero winners, because no answer
|
||||
}
|
||||
|
||||
var wonAnswers = chooseWonAnswers(round, room.RoundWinnersCount)
|
||||
var players = room.Players
|
||||
for _, answer := range wonAnswers {
|
||||
for _, player := range players {
|
||||
if answer.Wallet.Equals(player.Wallet) {
|
||||
player.RoundsWon++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
room.Players = players
|
||||
|
||||
var result string
|
||||
for i, answer := range wonAnswers {
|
||||
result += fmt.Sprintf("place:%d, winner:%s, votes:%s\n", i, answer.Wallet, answer.Votes)
|
||||
}
|
||||
|
||||
sendMessageToPlayers("RoundWinners", result)
|
||||
|
||||
// todo: Отправка награды победителям
|
||||
|
||||
room.Status = RoomStatusGaming // Next game cycle available to AskQuestion
|
||||
setRoom(ctx, &room)
|
||||
return true
|
||||
}
|
||||
|
||||
func automaticFinishGame(ctx storage.Context, room *Room, voted int) bool {
|
||||
if voted != len(room.Players) {
|
||||
return false // All players must have voted to finish the game
|
||||
}
|
||||
|
||||
return finishGame(ctx, room)
|
||||
}
|
||||
|
||||
func VoteToFinishGame(roomId string) bool {
|
||||
var ctx = storage.GetContext()
|
||||
var room = getRoom(ctx, roomId)
|
||||
var wallet = getSender()
|
||||
|
||||
if room.Host.Equals(wallet) {
|
||||
return false // Host cannot vote to finish game
|
||||
}
|
||||
|
||||
var voted = 0
|
||||
var isFound = false
|
||||
for i, p := range room.Players {
|
||||
if p.Wallet.Equals(wallet) {
|
||||
if p.IsVotedToFinish {
|
||||
return false // Player has already voted to finish the game
|
||||
}
|
||||
room.Players[i].IsVotedToFinish = true
|
||||
isFound = true
|
||||
}
|
||||
|
||||
if p.IsVotedToFinish {
|
||||
voted++ // Count voted players to finish the game
|
||||
}
|
||||
}
|
||||
|
||||
if !isFound {
|
||||
return false // Player was not found in the room
|
||||
}
|
||||
|
||||
// todo: Можно добавить notify для уведомления, сколько людей проголосовало за завершение игры
|
||||
|
||||
return automaticFinishGame(ctx, &room, voted)
|
||||
}
|
||||
|
||||
func ManuallyFinishGame(roomId string) bool {
|
||||
var ctx = storage.GetContext()
|
||||
var room = getRoom(ctx, roomId)
|
||||
|
||||
if !room.Host.Equals(getSender()) || room.Status != RoomStatusGaming {
|
||||
return false // Only host can finish game, room status must be gaming
|
||||
}
|
||||
|
||||
return finishGame(ctx, &room)
|
||||
}
|
||||
|
||||
func finishGame(ctx storage.Context, room *Room) bool {
|
||||
var winners = getGameWinner(room)
|
||||
|
||||
// todo: Отправка награды победителям и хосту
|
||||
|
||||
// todo: Нотификация результатов всей игры
|
||||
|
||||
room.Status = RoomStatusFinished
|
||||
setRoom(ctx, room)
|
||||
return true
|
||||
}
|
||||
|
||||
// todo: @vr61v /check
|
||||
func getGameWinner(room *Room) []Player {
|
||||
var players = room.Players
|
||||
var winners []Player
|
||||
var GameWinnersCount = room.GameWinnersCount
|
||||
|
||||
sort.Slice(players, func(a, b int) bool {
|
||||
return players[a].RoundsWon > players[b].RoundsWon
|
||||
})
|
||||
|
||||
if GameWinnersCount > len(room.Players) {
|
||||
return room.Players
|
||||
}
|
||||
|
||||
if GameWinnersCount == 1 {
|
||||
return []Player{players[0]}
|
||||
}
|
||||
|
||||
var lastWinner = players[0].RoundsWon
|
||||
winners = append(winners, players[0])
|
||||
for i := 1; i < len(players) && len(winners) < GameWinnersCount; i++ {
|
||||
var currentVote = players[i].RoundsWon
|
||||
if lastWinner == currentVote {
|
||||
GameWinnersCount++ // todo: Могут возникнуть проблемы, надо протестировать
|
||||
}
|
||||
lastWinner = currentVote
|
||||
winners = append(winners, players[i])
|
||||
}
|
||||
|
||||
return winners
|
||||
}
|
||||
|
||||
/* todo: Логика выкидывания игрока из игры, если пропускает несколько вопросов подряд -
|
||||
* делаем булевый флаг по которому определяем, активен игрок или нет.
|
||||
*/
|
Loading…
Add table
Reference in a new issue