add diffectly

This commit is contained in:
2026-10-03 14:52:40 +07:00
parent 4631c443e0
commit 7d4361a315
13 changed files with 200 additions and 103 deletions
+40 -37
View File
@@ -1045,19 +1045,19 @@ message DeleteAccountRsp {
message GetTermsReq {}
message GetTermsRsp {
string text = 1;
string text = 1;
}
message GetPrivacyReq {}
message GetPrivacyRsp {
string text = 1;
string text = 1;
}
message GetConsentReq {}
message GetConsentRsp {
string text = 1;
string text = 1;
}
message GetUsersReq {}
@@ -1080,13 +1080,13 @@ message GetUserByIDReq {
}
message GetUserByIDRsp {
User user = 1;
User user = 1;
}
message GetMeReq {}
message GetMeRsp {
User user = 1;
User user = 1;
}
message ChangeUserPasswordReq {
@@ -1139,7 +1139,7 @@ message AddScenarioReq {
}
message AddScenarioRsp {
int32 ID = 1;
int32 ID = 1;
}
message GetMyScenariosReq {}
@@ -1175,14 +1175,15 @@ message Scenario {
google.protobuf.Timestamp published_at = 10;
double average_rating = 11;
int32 reviews_count = 12;
string difficulty = 13;
}
message Story {
repeated Place places = 1;
Introduction introduction = 2;
repeated Question questions = 3;
string background = 4;
Conclusion conclusion = 5; // Только в полном сценарии, игрокам не передаётся.
repeated Place places = 1;
Introduction introduction = 2;
repeated Question questions = 3;
string background = 4;
Conclusion conclusion = 5; // Только в полном сценарии, игрокам не передаётся.
}
message Conclusion {
@@ -1364,20 +1365,20 @@ message DownloadScenarioArchiveReq {
}
message UploadScenarioArchiveRsp {
int32 ID = 1;
int32 ID = 1;
}
message AddGameReq {
string name = 1;
string description = 2;
google.protobuf.Timestamp start_at = 3;
int32 scenarioID = 4;
int32 scenarioID = 4;
string format = 5;
string location = 6;
}
message AddGameRsp {
int32 ID = 1;
int32 ID = 1;
}
message GetGamesReq {}
@@ -1437,7 +1438,7 @@ enum ScreenCommand {
message ScreenState {
string ID = 1;
string status = 2;
optional int32 gameID = 3;
optional int32 gameID = 3;
ScreenMode mode = 4;
bool sound_enabled = 5;
string error = 6;
@@ -1459,7 +1460,7 @@ message ScreenState {
message CreateScreenSessionReq {}
message CreateScreenSessionRsp {
string screenID = 1;
string screenID = 1;
string access_token = 2;
}
@@ -1507,7 +1508,7 @@ message SetScreenSoundRsp {
message MapItem {
string kind = 1;
int32 itemID = 2;
int32 itemID = 2;
string label = 3;
int32 x = 4;
int32 y = 5;
@@ -1601,22 +1602,22 @@ message ParticipationRsp {
}
message Team {
int32 ID = 1;
string name = 2;
string status = 3;
string password = 4;
int32 actions_count = 5;
repeated Application applications = 6;
string captain_email = 7;
int32 participants_count = 8;
int32 ID = 1;
string name = 2;
string status = 3;
string password = 4;
int32 actions_count = 5;
repeated Application applications = 6;
string captain_email = 7;
int32 participants_count = 8;
// Статус участия: pending, confirmed или cancelled. Поле панели организатора.
string participation_status = 9;
string participation_status = 9;
// Источник решения: captain, automatic либо пустая строка до ответа.
string participation_source = 10;
// Последняя ошибка отправки; пустая строка, если ошибки нет.
string participation_email_error = 11;
// Письмо принято SMTP-сервером; не означает прочтение или доставку во входящие.
bool participation_email_sent = 12;
bool participation_email_sent = 12;
}
message GetGameReq {
@@ -1624,7 +1625,7 @@ message GetGameReq {
}
message GetGameRsp {
Game game = 1;
Game game = 1;
}
message GetGameAnswersReq {
@@ -1668,22 +1669,24 @@ message GetGameResultsRsp {
message GameReview {
int32 ID = 1;
int32 gameID = 2;
int32 teamID = 3;
int32 gameID = 2;
int32 teamID = 3;
string team_name = 4;
int32 rating = 5;
string text = 6;
string status = 7;
google.protobuf.Timestamp created_at = 8;
string game_name = 9;
int32 difficulty = 10;
}
message CreateGameReviewReq {
int32 gameID = 1;
int32 teamID = 2;
string password = 3;
int32 rating = 4;
string text = 5;
int32 gameID = 1;
int32 teamID = 2;
string password = 3;
int32 rating = 4;
string text = 5;
int32 difficulty = 6;
}
message CreateGameReviewRsp {
@@ -1720,7 +1723,7 @@ message UpdateGameReq {
string name = 2;
string description = 3;
google.protobuf.Timestamp start_at = 4;
int32 scenarioID = 5;
int32 scenarioID = 5;
string format = 6;
string location = 7;
}
@@ -1771,7 +1774,7 @@ message DeleteGameRsp {
}
message AddTeamReq {
int32 gameID = 1;
int32 gameID = 1;
string name = 2;
string captain_email = 3;
}
Binary file not shown.
@@ -3118,6 +3118,10 @@
},
"text": {
"type": "string"
},
"difficulty": {
"type": "integer",
"format": "int32"
}
}
},
@@ -3710,6 +3714,10 @@
},
"gameName": {
"type": "string"
},
"difficulty": {
"type": "integer",
"format": "int32"
}
}
},
@@ -4293,6 +4301,9 @@
"reviewsCount": {
"type": "integer",
"format": "int32"
},
"difficulty": {
"type": "string"
}
}
},
+11 -10
View File
@@ -13,15 +13,16 @@ import (
func mapGameReview(r *game_review_service.Review) *proto.GameReview {
return &proto.GameReview{
ID: int32(r.ID),
GameID: int32(r.GameID),
TeamID: int32(r.TeamID),
TeamName: r.TeamName,
Rating: r.Rating,
Text: r.Text,
Status: r.Status,
CreatedAt: timestamppb.New(r.CreatedAt),
GameName: r.GameName,
ID: int32(r.ID),
GameID: int32(r.GameID),
TeamID: int32(r.TeamID),
TeamName: r.TeamName,
Rating: r.Rating,
Difficulty: r.Difficulty,
Text: r.Text,
Status: r.Status,
CreatedAt: timestamppb.New(r.CreatedAt),
GameName: r.GameName,
}
}
@@ -34,7 +35,7 @@ func mapGameReviews(reviews []*game_review_service.Review) []*proto.GameReview {
}
func (s *server) CreateGameReview(ctx context.Context, req *proto.CreateGameReviewReq) (*proto.CreateGameReviewRsp, error) {
review, err := s.reviewService.Create(ctx, int(req.GameID), int(req.TeamID), req.Password, req.Rating, req.Text)
review, err := s.reviewService.Create(ctx, int(req.GameID), int(req.TeamID), req.Password, req.Rating, req.Difficulty, req.Text)
if err != nil {
return nil, reviewErrorStatus(err, "не удалось создать отзыв")
}
+2 -1
View File
@@ -97,12 +97,13 @@ func (s *server) GetScenario(ctx context.Context, req *proto.GetScenarioReq) (*p
func (s *server) addScenarioRatings(ctx context.Context, scenarios []*scenarios_service.Scenario) error {
for _, scenario := range scenarios {
average, count, err := s.reviewService.ScenarioRating(ctx, scenario.ID)
average, count, difficulty, err := s.reviewService.ScenarioRating(ctx, scenario.ID)
if err != nil {
return err
}
scenario.AverageRating = average
scenario.ReviewsCount = count
scenario.Difficulty = difficulty
}
return nil
}
+1
View File
@@ -40,6 +40,7 @@ func mapScenario(o *scenarios_service.Scenario) *proto.Scenario {
PublishedAt: publishedAt,
AverageRating: o.AverageRating,
ReviewsCount: o.ReviewsCount,
Difficulty: o.Difficulty,
}
}
+29 -26
View File
@@ -23,15 +23,16 @@ var (
// Review describes a game review.
type Review struct {
ID int
GameID int
GameName string
TeamID int
TeamName string
Rating int32
Text string
Status string
CreatedAt time.Time
ID int
GameID int
GameName string
TeamID int
TeamName string
Rating int32
Difficulty int32
Text string
Status string
CreatedAt time.Time
}
// Repo persists game reviews.
@@ -41,13 +42,13 @@ type Repo struct{ pool *pgxpool.Pool }
func NewGameReviewsRepo(pool *pgxpool.Pool) *Repo { return &Repo{pool: pool} }
// Create stores a review.
func (r *Repo) Create(ctx context.Context, gameID, teamID int, rating int32, text string) (*Review, error) {
func (r *Repo) Create(ctx context.Context, gameID, teamID int, rating, difficulty int32, text string) (*Review, error) {
review := &Review{}
err := r.pool.QueryRow(
ctx,
`
INSERT INTO game_reviews (game_id, team_id, rating, text)
SELECT $1, t.id, $3, $4
INSERT INTO game_reviews (game_id, team_id, rating, difficulty, text)
SELECT $1, t.id, $3, $4, $5
FROM teams t
WHERE t.id = $2 AND t.game_id = $1
ON CONFLICT (game_id, team_id) DO NOTHING
@@ -55,9 +56,10 @@ func (r *Repo) Create(ctx context.Context, gameID, teamID int, rating int32, tex
gameID,
teamID,
rating,
difficulty,
text,
).Scan(
&review.ID, &review.GameID, &review.TeamID, &review.Rating, &review.Text, &review.Status, &review.CreatedAt)
&review.ID, &review.GameID, &review.TeamID, &review.Rating, &review.Difficulty, &review.Text, &review.Status, &review.CreatedAt)
if errors.Is(err, pgx.ErrNoRows) {
var exists bool
if err := r.pool.QueryRow(
@@ -89,7 +91,7 @@ func (r *Repo) GetByID(ctx context.Context, id int) (*Review, error) {
func (r *Repo) List(ctx context.Context, gameID int, all bool) ([]*Review, float64, int32, error) {
rows, err := r.pool.Query(
ctx,
`SELECT r.id, r.game_id, r.team_id, t.name, g.name, r.rating, r.text, r.status, r.created_at
`SELECT r.id, r.game_id, r.team_id, t.name, g.name, r.rating, r.difficulty, r.text, r.status, r.created_at
FROM game_reviews r
JOIN teams t ON t.id = r.team_id
JOIN games g ON g.id = r.game_id
@@ -105,7 +107,7 @@ func (r *Repo) List(ctx context.Context, gameID int, all bool) ([]*Review, float
var reviews []*Review
for rows.Next() {
review := &Review{}
if err := rows.Scan(&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.GameName, &review.Rating, &review.Text, &review.Status, &review.CreatedAt); err != nil {
if err := rows.Scan(&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.GameName, &review.Rating, &review.Difficulty, &review.Text, &review.Status, &review.CreatedAt); err != nil {
return nil, 0, 0, err
}
reviews = append(reviews, review)
@@ -131,7 +133,7 @@ func (r *Repo) List(ctx context.Context, gameID int, all bool) ([]*Review, float
func (r *Repo) ListForModeration(ctx context.Context) ([]*Review, error) {
rows, err := r.pool.Query(
ctx,
`SELECT r.id, r.game_id, r.team_id, t.name, g.name, r.rating, r.text, r.status, r.created_at
`SELECT r.id, r.game_id, r.team_id, t.name, g.name, r.rating, r.difficulty, r.text, r.status, r.created_at
FROM game_reviews r
JOIN teams t ON t.id = r.team_id
JOIN games g ON g.id = r.game_id
@@ -144,7 +146,7 @@ func (r *Repo) ListForModeration(ctx context.Context) ([]*Review, error) {
var reviews []*Review
for rows.Next() {
review := &Review{}
if err := rows.Scan(&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.GameName, &review.Rating, &review.Text, &review.Status, &review.CreatedAt); err != nil {
if err := rows.Scan(&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.GameName, &review.Rating, &review.Difficulty, &review.Text, &review.Status, &review.CreatedAt); err != nil {
return nil, err
}
reviews = append(reviews, review)
@@ -175,25 +177,26 @@ func (r *Repo) Moderate(ctx context.Context, id int, status string) (*Review, er
}
// ScenarioRating returns aggregate ratings for a scenario.
func (r *Repo) ScenarioRating(ctx context.Context, scenarioID int) (float64, int32, error) {
func (r *Repo) ScenarioRating(ctx context.Context, scenarioID int) (float64, int32, float64, error) {
var average float64
var averageDifficulty float64
var count int64
err := r.pool.QueryRow(
ctx,
`SELECT COALESCE(AVG(r.rating), 0), COUNT(*)
`SELECT COALESCE(AVG(r.rating), 0), COUNT(*), COALESCE(AVG(r.difficulty), 0)
FROM game_reviews r
JOIN games g ON g.id = r.game_id
WHERE g.scenario_id = $1 AND g.status = 'finish' AND r.status = 'approved'`,
scenarioID,
).Scan(&average, &count)
return average, int32(count), err
).Scan(&average, &count, &averageDifficulty)
return average, int32(count), averageDifficulty, err
}
// ListByScenario returns reviews and aggregate ratings for a scenario.
func (r *Repo) ListByScenario(ctx context.Context, scenarioID int) ([]*Review, float64, int32, error) {
rows, err := r.pool.Query(
ctx,
`SELECT r.id, r.game_id, r.team_id, t.name, g.name, r.rating, r.text, r.status, r.created_at
`SELECT r.id, r.game_id, r.team_id, t.name, g.name, r.rating, r.difficulty, r.text, r.status, r.created_at
FROM game_reviews r
JOIN teams t ON t.id = r.team_id
JOIN games g ON g.id = r.game_id
@@ -208,7 +211,7 @@ func (r *Repo) ListByScenario(ctx context.Context, scenarioID int) ([]*Review, f
var reviews []*Review
for rows.Next() {
review := &Review{}
if err := rows.Scan(&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.GameName, &review.Rating, &review.Text, &review.Status, &review.CreatedAt); err != nil {
if err := rows.Scan(&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.GameName, &review.Rating, &review.Difficulty, &review.Text, &review.Status, &review.CreatedAt); err != nil {
return nil, 0, 0, err
}
reviews = append(reviews, review)
@@ -216,7 +219,7 @@ func (r *Repo) ListByScenario(ctx context.Context, scenarioID int) ([]*Review, f
if err := rows.Err(); err != nil {
return nil, 0, 0, err
}
average, count, err := r.ScenarioRating(ctx, scenarioID)
average, count, _, err := r.ScenarioRating(ctx, scenarioID)
return reviews, average, count, err
}
@@ -224,14 +227,14 @@ func (r *Repo) queryOne(ctx context.Context, id int) (*Review, error) {
review := &Review{}
err := r.pool.QueryRow(
ctx,
`SELECT r.id, r.game_id, r.team_id, t.name, g.name, r.rating, r.text, r.status, r.created_at
`SELECT r.id, r.game_id, r.team_id, t.name, g.name, r.rating, r.difficulty, r.text, r.status, r.created_at
FROM game_reviews r
JOIN teams t ON t.id = r.team_id
JOIN games g ON g.id = r.game_id
WHERE r.id = $1`,
id,
).Scan(
&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.GameName, &review.Rating, &review.Text, &review.Status, &review.CreatedAt)
&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.GameName, &review.Rating, &review.Difficulty, &review.Text, &review.Status, &review.CreatedAt)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrReviewNotFound
}
+20 -18
View File
@@ -9,15 +9,16 @@ import (
// Review describes a game review exposed by the service layer.
type Review struct {
ID int
GameID int
GameName string
TeamID int
TeamName string
Rating int32
Text string
Status string
CreatedAt time.Time
ID int
GameID int
GameName string
TeamID int
TeamName string
Rating int32
Difficulty int32
Text string
Status string
CreatedAt time.Time
}
func mapReview(review *game_reviews_repo.Review) *Review {
@@ -25,15 +26,16 @@ func mapReview(review *game_reviews_repo.Review) *Review {
return nil
}
return &Review{
ID: review.ID,
GameID: review.GameID,
GameName: review.GameName,
TeamID: review.TeamID,
TeamName: review.TeamName,
Rating: review.Rating,
Text: review.Text,
Status: review.Status,
CreatedAt: review.CreatedAt,
ID: review.ID,
GameID: review.GameID,
GameName: review.GameName,
TeamID: review.TeamID,
TeamName: review.TeamName,
Rating: review.Rating,
Difficulty: review.Difficulty,
Text: review.Text,
Status: review.Status,
CreatedAt: review.CreatedAt,
}
}
@@ -31,7 +31,7 @@ var (
// ErrGameNotFinished indicates that the game is not finished.
ErrGameNotFinished = errors.New("оценить можно только завершённую игру")
// ErrInvalidReview indicates that review data is invalid.
ErrInvalidReview = errors.New("оценка должна быть от 1 до 5, текст обязателен")
ErrInvalidReview = errors.New("оценка должна быть от 1 до 5, сложность от 1 до 3, текст обязателен")
)
// GameReviewService handles game reviews.
@@ -49,8 +49,8 @@ func NewGameReviewService(reviews *game_reviews_repo.Repo, games *games_repo.Gam
}
// Create stores a game review.
func (s *GameReviewService) Create(ctx context.Context, gameID, teamID int, password string, rating int32, text string) (*Review, error) {
if rating < 1 || rating > 5 || strings.TrimSpace(text) == "" {
func (s *GameReviewService) Create(ctx context.Context, gameID, teamID int, password string, rating, difficulty int32, text string) (*Review, error) {
if rating < 1 || rating > 5 || difficulty < 1 || difficulty > 3 || strings.TrimSpace(text) == "" {
return nil, ErrInvalidReview
}
team, err := s.teams.GetTeamByID(ctx, teamID)
@@ -73,7 +73,7 @@ func (s *GameReviewService) Create(ctx context.Context, gameID, teamID int, pass
if game.Status != "finish" {
return nil, ErrGameNotFinished
}
review, err := s.reviews.Create(ctx, gameID, teamID, rating, strings.TrimSpace(text))
review, err := s.reviews.Create(ctx, gameID, teamID, rating, difficulty, strings.TrimSpace(text))
if err != nil {
if errors.Is(err, game_reviews_repo.ErrReviewExists) {
return nil, ErrReviewExists
@@ -117,8 +117,25 @@ func (s *GameReviewService) Moderate(ctx context.Context, id int, status string)
}
// ScenarioRating returns aggregate ratings for a scenario.
func (s *GameReviewService) ScenarioRating(ctx context.Context, scenarioID int) (float64, int32, error) {
return s.reviews.ScenarioRating(ctx, scenarioID)
func (s *GameReviewService) ScenarioRating(ctx context.Context, scenarioID int) (float64, int32, string, error) {
average, count, averageDifficulty, err := s.reviews.ScenarioRating(ctx, scenarioID)
if err != nil {
return 0, 0, "", err
}
return average, count, difficultyCategory(averageDifficulty, count), nil
}
func difficultyCategory(average float64, count int32) string {
if count == 0 {
return ""
}
if average <= 1.66 {
return "лёгкая"
}
if average <= 2.33 {
return "средняя"
}
return "тяжёлая"
}
// ListByScenario returns reviews for a scenario.
@@ -33,3 +33,20 @@ func TestReviewReminderEmail(t *testing.T) {
})
}
}
func TestDifficultyCategory(t *testing.T) {
for _, test := range []struct {
average float64
count int32
want string
}{
{average: 0, count: 0, want: ""},
{average: 1.66, count: 1, want: "лёгкая"},
{average: 2, count: 1, want: "средняя"},
{average: 2.34, count: 1, want: "тяжёлая"},
} {
if got := difficultyCategory(test.average, test.count); got != test.want {
t.Errorf("difficultyCategory(%v, %d) = %q, want %q", test.average, test.count, got, test.want)
}
}
}
@@ -21,5 +21,6 @@ type Scenario struct {
PublishedAt *time.Time
AverageRating float64
ReviewsCount int32
Difficulty string
IsDeleted bool
}
@@ -0,0 +1,6 @@
-- +goose Up
ALTER TABLE game_reviews
ADD COLUMN difficulty INTEGER NOT NULL DEFAULT 2 CHECK (difficulty BETWEEN 1 AND 3);
-- +goose Down
ALTER TABLE game_reviews DROP COLUMN difficulty;
+39 -5
View File
@@ -2264,6 +2264,7 @@ type Scenario struct {
PublishedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=published_at,json=publishedAt,proto3" json:"published_at,omitempty"`
AverageRating float64 `protobuf:"fixed64,11,opt,name=average_rating,json=averageRating,proto3" json:"average_rating,omitempty"`
ReviewsCount int32 `protobuf:"varint,12,opt,name=reviews_count,json=reviewsCount,proto3" json:"reviews_count,omitempty"`
Difficulty string `protobuf:"bytes,13,opt,name=difficulty,proto3" json:"difficulty,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -2382,6 +2383,13 @@ func (x *Scenario) GetReviewsCount() int32 {
return 0
}
func (x *Scenario) GetDifficulty() string {
if x != nil {
return x.Difficulty
}
return ""
}
type Story struct {
state protoimpl.MessageState `protogen:"open.v1"`
Places []*Place `protobuf:"bytes,1,rep,name=places,proto3" json:"places,omitempty"`
@@ -6734,6 +6742,7 @@ type GameReview struct {
Status string `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
GameName string `protobuf:"bytes,9,opt,name=game_name,json=gameName,proto3" json:"game_name,omitempty"`
Difficulty int32 `protobuf:"varint,10,opt,name=difficulty,proto3" json:"difficulty,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -6831,6 +6840,13 @@ func (x *GameReview) GetGameName() string {
return ""
}
func (x *GameReview) GetDifficulty() int32 {
if x != nil {
return x.Difficulty
}
return 0
}
type CreateGameReviewReq struct {
state protoimpl.MessageState `protogen:"open.v1"`
GameID int32 `protobuf:"varint,1,opt,name=gameID,proto3" json:"gameID,omitempty"`
@@ -6838,6 +6854,7 @@ type CreateGameReviewReq struct {
Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"`
Rating int32 `protobuf:"varint,4,opt,name=rating,proto3" json:"rating,omitempty"`
Text string `protobuf:"bytes,5,opt,name=text,proto3" json:"text,omitempty"`
Difficulty int32 `protobuf:"varint,6,opt,name=difficulty,proto3" json:"difficulty,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -6907,6 +6924,13 @@ func (x *CreateGameReviewReq) GetText() string {
return ""
}
func (x *CreateGameReviewReq) GetDifficulty() int32 {
if x != nil {
return x.Difficulty
}
return 0
}
type CreateGameReviewRsp struct {
state protoimpl.MessageState `protogen:"open.v1"`
Review *GameReview `protobuf:"bytes,1,opt,name=review,proto3" json:"review,omitempty"`
@@ -9448,7 +9472,7 @@ const file_main_proto_rawDesc = "" +
"\x0eGetScenarioReq\x12\x0e\n" +
"\x02ID\x18\x01 \x01(\x05R\x02ID\"V\n" +
"\x0eGetScenarioRsp\x12D\n" +
"\bscenario\x18\x01 \x01(\v2(.crabs.evening_detective_server.ScenarioR\bscenario\"\x9e\x04\n" +
"\bscenario\x18\x01 \x01(\v2(.crabs.evening_detective_server.ScenarioR\bscenario\"\xbe\x04\n" +
"\bScenario\x12\x0e\n" +
"\x02ID\x18\x01 \x01(\x05R\x02ID\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12%\n" +
@@ -9464,7 +9488,10 @@ const file_main_proto_rawDesc = "" +
"\fpublished_at\x18\n" +
" \x01(\v2\x1a.google.protobuf.TimestampR\vpublishedAt\x12%\n" +
"\x0eaverage_rating\x18\v \x01(\x01R\raverageRating\x12#\n" +
"\rreviews_count\x18\f \x01(\x05R\freviewsCountB\x0e\n" +
"\rreviews_count\x18\f \x01(\x05R\freviewsCount\x12\x1e\n" +
"\n" +
"difficulty\x18\r \x01(\tR\n" +
"difficultyB\x0e\n" +
"\f_descriptionB\b\n" +
"\x06_image\"\xcc\x02\n" +
"\x05Story\x12=\n" +
@@ -9753,7 +9780,7 @@ const file_main_proto_rawDesc = "" +
"\ractions_count\x18\x05 \x01(\x05R\factionsCount\"\xa4\x01\n" +
"\x11GetGameResultsRsp\x12M\n" +
"\tquestions\x18\x01 \x03(\v2/.crabs.evening_detective_server.ResultsQuestionR\tquestions\x12@\n" +
"\x05teams\x18\x02 \x03(\v2*.crabs.evening_detective_server.TeamResultR\x05teams\"\x85\x02\n" +
"\x05teams\x18\x02 \x03(\v2*.crabs.evening_detective_server.TeamResultR\x05teams\"\xa5\x02\n" +
"\n" +
"GameReview\x12\x0e\n" +
"\x02ID\x18\x01 \x01(\x05R\x02ID\x12\x16\n" +
@@ -9765,13 +9792,20 @@ const file_main_proto_rawDesc = "" +
"\x06status\x18\a \x01(\tR\x06status\x129\n" +
"\n" +
"created_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x1b\n" +
"\tgame_name\x18\t \x01(\tR\bgameName\"\x8d\x01\n" +
"\tgame_name\x18\t \x01(\tR\bgameName\x12\x1e\n" +
"\n" +
"difficulty\x18\n" +
" \x01(\x05R\n" +
"difficulty\"\xad\x01\n" +
"\x13CreateGameReviewReq\x12\x16\n" +
"\x06gameID\x18\x01 \x01(\x05R\x06gameID\x12\x16\n" +
"\x06teamID\x18\x02 \x01(\x05R\x06teamID\x12\x1a\n" +
"\bpassword\x18\x03 \x01(\tR\bpassword\x12\x16\n" +
"\x06rating\x18\x04 \x01(\x05R\x06rating\x12\x12\n" +
"\x04text\x18\x05 \x01(\tR\x04text\"Y\n" +
"\x04text\x18\x05 \x01(\tR\x04text\x12\x1e\n" +
"\n" +
"difficulty\x18\x06 \x01(\x05R\n" +
"difficulty\"Y\n" +
"\x13CreateGameReviewRsp\x12B\n" +
"\x06review\x18\x01 \x01(\v2*.crabs.evening_detective_server.GameReviewR\x06review\"+\n" +
"\x11GetGameReviewsReq\x12\x16\n" +