diff --git a/room_contract.go b/room_contract.go new file mode 100644 index 0000000..3150a36 --- /dev/null +++ b/room_contract.go @@ -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: Логика выкидывания игрока из игры, если пропускает несколько вопросов подряд - + * делаем булевый флаг по которому определяем, активен игрок или нет. + */