aboutsummaryrefslogtreecommitdiff
path: root/content/posts/2024-12-12-guessr/index.md
blob: 6be917d2ca387516d7c10eb2da4c36121f945b8c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
---
categories:
- Мои проекты
date: "2024-12-12T22:27:49+03:00"
description: ""
image: logo.webp
location: Казань
tags:
- IT
- Проект выходного дня
title: Guessr
---

# Guessr

На недавних выходных я запилил очередной «проект выходного дня». На этот раз —
аналог известного сервиса GeoGuessr, но в отличие от него, все точки
сконцентрированы в моей родной Казани. Ну и я не использую панорамы, а
фотографии мест.

Я обещал выложить исходники, и в общем, вот они:
https://git.neonxp.ru/guessr.git/

## Немного про разработку

Первым встал вопрос, откуда брать данные, а именно фотографии и координаты
точек. Пару лет назад нашу страну покинул такой проект, как Ingress,
представлявший собой гео игру в дополненной реальности. В свою очередь, я
посчитал, что раз проект решил отказаться от нас, как игроков, я посчитал
морально оправданным ~~спиз~~экспропреировать кусочек их данных, а именно
спарсил с их карты intel.ingress.com т.н. «порталы», которые, по сути и есть эти
самые геоточки с фотографиями.

Дамп я загнал в Postgresql с подключенным расширением
[Postgis](https://postgis.net/).

Ну а далее написал достаточно простой API на Golang, который реализует следующие
методы:

- Создание новой игровой сессии, в ответ ставится кука внутри которой
  зашифровано текущее состояние — ник, количество очков, ID текущего
  угадываемого объекта (в начале пустое).
  ```http
  POST /api/state
  Content-Type: application/json

  {
      "username": "NeonXP"
  }
  ```
- Получение состояния. Просто возвращает вышеуказанные параметры
  ```http
  GET /api/state
  ```
- Выдача нового объекта для угадывания. При этом возвращается ссылка на фото и
  обновляется состояние, тем что в него вписывается ID объекта
  ```http
  POST /api/next
  ```
- Угадывание. Собственно, на вход передаются координаты куда на карте указал
  игрок. А в ответ возвращается:
  - Название объекта
  - Расстояние от переданной точки до реального размещения объекта
  - Geojson строка в которой зашифрована линия соединяющая точку и объект (нужна
    для отрисовки красной линии на карте)

  При этом высчитываются очки которые получает игрок за попытку по формуле
  max(1000-d, 0), где d - расстояние между выбранной точкой и объектом в метрах.
  То есть, если разница меньше 1000м, то чем ближе - тем больше очков (максимум
  1000 очков за 1 очень точное угадывание).
  ```http
  POST /api/guess
  Content-Type: application/json

  {
      "lat": 55.123,
      "lon": 49.123
  }
  ```

Вот в общем-то и всё API!

Из интересностей, при выборе очередной точки у неё в БД увеличивается счетчик, а
сам select выбирает случайную точку только среди тех точек, где этот счетчик
минимальный. То есть, пока не будут выданы игрокам все точки, уже выбранные
заново не будут выданы. Вот это место в коде:
https://git.neonxp.ru/guessr.git/tree/pkg/service/places.go#n26 (стр. 26-32)

```go
err = btx.NewSelect().
    ColumnExpr(`p.guid, p.img`).
    Model(r).
    Where(`p.count = (SELECT MIN(pl.count) FROM places pl WHERE pl.deleted_at IS NULL)`).
    OrderExpr(`RANDOM()`).
    Limit(1).
    Scan(ctx, r)
```

Ещё я бы отметил то, что я решил по максимуму логику вынести в БД, и, например,
при угадывании расстояние до точки, а также вышеупомянутый geojson формируются
так же на стороне БД:
https://git.neonxp.ru/guessr.git/tree/pkg/service/places.go#n50 (стр. 50-59)

```go
err := p.db.NewSelect().
		Model(&model.Place{GUID: guid}).
		WherePK("guid").
		ColumnExpr(`p.name, p.guid, p.img,
			ST_Distance(ST_MakePoint(?, ?)::geography, p.position::geography)::int AS distance,
			ST_AsGeoJSON(ST_MakeLine(
				ST_SetSRID(ST_MakePoint(?, ?), 4326),
				ST_SetSRID(p.position, 4326)
			)) AS geojson`, lon, lat, lon, lat).
		Scan(ctx, r)
```

## Дальнейшие планы

В комментах к анонсу ребята накидали достаточно много хороших идей, синтезировав
которые, и добавив свои хотелки я составил примерно такой чеклист:

- [ ] Авторизация и общая доска лидерства
- [ ] После угадывания спрашивать у игрока «сложность», чтобы потом можно было,
  например, настраивать чтобы попадались только простые объекты. И, например,
  разное количество очков за простые и сложные объекты
- [ ] Подумать как вынести игру в оффлайн, по типу того же ингресса. Это сложно
  и предстоит хорошо это обдумать

Как-то так :) А впереди новые выходные и новые «проекты выходного дня»!