diff --git a/TestContract/go.mod b/TestContract/go.mod new file mode 100644 index 0000000..dfb83b7 --- /dev/null +++ b/TestContract/go.mod @@ -0,0 +1,5 @@ +module TestContract + +go 1.22 + +require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20241223145456-80e18222bca2 diff --git a/TestContract/go.sum b/TestContract/go.sum new file mode 100644 index 0000000..75af27b --- /dev/null +++ b/TestContract/go.sum @@ -0,0 +1,2 @@ +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20241223145456-80e18222bca2 h1:4Bfi6A1kPpaTDuwbDVc6x+R4WXgoNN9wIq6XobDlXHs= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20241223145456-80e18222bca2/go.mod h1:kVLzmbeJJdbIPF2bUYhD8YppIiLXnRQj5yqNZvzbOL0= diff --git a/TestContract/main.go b/TestContract/main.go new file mode 100644 index 0000000..e2739ed --- /dev/null +++ b/TestContract/main.go @@ -0,0 +1,309 @@ +package TestContract + +import ( + "fmt" + "github.com/google/uuid" + "github.com/nspcc-dev/neo-go/pkg/interop" + "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" +) + +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) +} + +// RoomStatusWaiting created room wait for join all players +// todo описать фазы +const ( + RoomStatusWaiting = "waiting" + // RoomStatusGameIsStarted todo мне не нравится это название фазы. оно должно описывать промежуточное состояние + //мб RoomStatusGaming + // еще до задачи вопроса и после завершения предыдущего раунда + RoomStatusGameIsStarted = "game_is_going" + RoomStatusRoundIsStarted = "round_is_started" + RoomStatusVoting = "voting" + RoomStatusFinished = "finished" +) + +type Room struct { + Id string + Host interop.Hash160 + Status string + PrizePool int + CountWinners int // ??? //мне кажется норм инт да, они там везде интом оперируют это даже в доке есть + Players []Player + Rounds []Round +} + +type Round struct { + number int // Sequence number of round + question string + answers []Answer +} + +type Answer struct { + wallet interop.Hash160 + answer string + votes int + answerIdx int // Index of sent answer to user - это что, кол-во данных ответов на вопрос? +} + +type Player struct { + Wallet interop.Hash160 //roundsWon - сколько раундов выиграл - мы тут не храним в итоге? +} + +func getRoom(ctx storage.Context, roomId string) *Room { + var roomData = storage.Get(ctx, []byte("room:"+roomId)) + if roomData == nil { + panic("Room with roomId=" + roomId + " not found") + } + + return roomData.(*Room) +} + +func getSender() interop.Hash160 { + return runtime.GetScriptContainer().Sender +} + +func CreateRoom(host interop.Hash160, countWinners int) string { + var ctx = storage.GetContext() + var id = uuid.NewString() + var room = Room{ + Id: id, + Host: host, + Status: RoomStatusWaiting, + PrizePool: 0, + CountWinners: countWinners, + Players: []Player{}, + Rounds: []Round{}, + } + + storage.Put(ctx, []byte("room:"+id), room) + return id + //нужно добавить списание денег +} + +func JoinRoom(roomId string, wallet interop.Hash160) bool { + var ctx = storage.GetContext() + var room = getRoom(ctx, roomId) + if room.Status != RoomStatusWaiting { + return false //Player cannot join started room + } + + for _, player := range room.Players { + if player.Wallet.Equals(wallet) { + return false // Player already joined room + } + } + + var player = Player{Wallet: wallet} + room.Players = append(room.Players, player) + storage.Put(ctx, []byte("room:"+roomId), room) + return true + //нужно добавить списание денег +} + +func StartGame(roomId string, wallet interop.Hash160) bool { + var ctx = storage.GetContext() + var room = getRoom(ctx, roomId) + + // Only host can start game, room status must be is waiting and players count must be > count winners + if room.Host.Equals(wallet) || room.Status != RoomStatusWaiting || len(room.Players) <= room.CountWinners { + return false //тут же вроде наоборот не equals + } + + room.Status = RoomStatusGameIsStarted + storage.Put(ctx, []byte("room:"+roomId), room) + return true +} + +func AskQuestion(roomId string, wallet interop.Hash160, question string) bool { + var ctx = storage.GetContext() + var room = getRoom(ctx, roomId) + + // Only host can ask question, room status must be game is started + if room.Host.Equals(wallet) || room.Status != RoomStatusGameIsStarted { + return false //тут же вроде наоборот не equals + } + + newRound := Round{ + number: len(room.Rounds) + 1, + question: question, + answers: []Answer{}, + } + room.Rounds = append(room.Rounds, newRound) + storage.Put(ctx, []byte("room:"+roomId), room) + return true +} + +// todo отсюда я начал писать + +func StartQuestion(roomId string) bool { + var ctx = storage.GetContext() + var room = getRoom(ctx, roomId) + + // Only host can start question, room status must be game is started + if !room.Host.Equals(getSender()) || room.Status != RoomStatusGameIsStarted { + return false + } + + room.Status = RoomStatusRoundIsStarted + + for _, player := range room.Players { + var question = room.Rounds[len(room.Rounds)-1].question + sendMessageToPlayer(question, player) + } + + storage.Put(ctx, []byte("room:"+roomId), room) + return true +} //тут еще про nns надо не забыть + +func SendAnswer(roomId string, answer string) bool { + var ctx = storage.GetContext() + var room = getRoom(ctx, roomId) + + // Only player can send answer, room status must be round is started + if roomContains(room.Players, getSender()) || room.Status != RoomStatusRoundIsStarted { + return false + } + + newAnswer := Answer{ + getSender(), + answer, + 0, + -1, + } + room.Rounds[len(room.Rounds)-1].answers = append(room.Rounds[len(room.Rounds)-1].answers, newAnswer) + + storage.Put(ctx, []byte("room:"+roomId), room) + return true +} + +// todo проверка отправки ответа (от каждого участника если отправлен ответ, то макс один) + +func roomContains(slice []Player, item interop.Hash160) bool { + for _, v := range slice { + if v.Wallet.Equals(item) { + return true + } + } + return false +} + +func EndQuestion(roomId string) bool { + var ctx = storage.GetContext() + var room = getRoom(ctx, roomId) + + // Only host can end question, room status must be round is started + if !room.Host.Equals(getSender()) || room.Status != RoomStatusRoundIsStarted { + return false + } + + room.Status = RoomStatusVoting + + var round = room.Rounds[len(room.Rounds)-1] + + var result string + for i, answer := range round.answers { + // todo временный комментарий: проставляем индексы ответам как их отправили в вывод, чтобы потом можно было соотнести + // интовый голос за ответ с игроком + answer.answerIdx = i + result += fmt.Sprintf("%d: %s\n", i, answer.answer) + } + + for _, player := range room.Players { + sendMessageToPlayer(result, player) + } + + storage.Put(ctx, []byte("room:"+roomId), room) + return true +} + +func ChooseAnswer(roomId string, answerIdx int) bool { + var ctx = storage.GetContext() + var room = getRoom(ctx, roomId) + + // Only host can choose answer, room status must be voting + if !room.Host.Equals(getSender()) || room.Status != RoomStatusVoting { + return false + } + + var round = room.Rounds[len(room.Rounds)-1] + + for _, answer := range round.answers { + if answer.answerIdx == answerIdx { + answer.votes += 1 + } + } + + storage.Put(ctx, []byte("room:"+roomId), room) + return true +} + +func sendMessageToPlayer(message string, player Player) { + // todo +} + +// todo можно прям сюда подсовывать ссылку на другой контракт кастомный и дефолт реализацию оставить в методе, а кастомную условием +// нам сказали, что ссылку на другой контракт нельзя чисто, используем nns +func GetWinner(roomId string) bool { + var ctx = storage.GetContext() + var room = getRoom(ctx, roomId) + + // Only host can get winner, room status must be voting + if !room.Host.Equals(getSender()) || room.Status != RoomStatusVoting { + return false + } + + var round = room.Rounds[len(room.Rounds)-1] + + if len(round.answers) == 0 { + panic(fmt.Sprintf("Round number %d does not have answers", len(room.Rounds))) + } + + winner := round.answers[0] + + // todo для mvp пока топ 1 победитель. потом сделаем CountWinners или кастомные реализации распределений + for _, answer := range round.answers { + if answer.votes > winner.votes { + winner = answer + } + } + + for _, player := range room.Players { + sendMessageToPlayer(winner.answer, player) + } + + // todo отправка награды + + room.Status = RoomStatusGameIsStarted // Next game cycle available. to AskQuestion + storage.Put(ctx, []byte("room:"+roomId), room) + return true +} + +func FinishGame(roomId string) bool { + var ctx = storage.GetContext() + var room = getRoom(ctx, roomId) + + // Only host can finish game, room status must be game is going + if !room.Host.Equals(getSender()) || room.Status != RoomStatusGameIsStarted { + return false + } + + // todo победитель всей игры? итоги? что нибудь еще? + + room.Status = RoomStatusFinished + storage.Put(ctx, []byte("room:"+roomId), room) + return true + // todo добавить вилку, что если ВСЕ участники согласились закончить игру - заканчиваем +} + +// todo логика выкидывания игрока из игры, если пропускает несколько вопросов подряд - делаем булевый флаг по которому определяем, активен игрок или нет diff --git a/TestContract/neo-go.yml b/TestContract/neo-go.yml new file mode 100644 index 0000000..8dc062a --- /dev/null +++ b/TestContract/neo-go.yml @@ -0,0 +1,11 @@ +name: TestContract +sourceurl: http://example.com/ +safemethods: [] +supportedstandards: [] +events: + - name: Hello world! + parameters: + - name: args + type: Array +permissions: + - methods: '*'