+++
title = "Guessr"
description = ""
date = 2024-12-12T22:27:49+03:00
categories = [ "Мои проекты" ]
tags = [ "IT", "Проект выходного дня" ]
location = "Казань"
image="logo.jpg"
+++
На недавних выходных я запилил очередной «проект выходного дня». На этот раз — аналог известного сервиса 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)
```
# Дальнейшие планы
В комментах к анонсу ребята накидали достаточно много хороших идей, синтезировав которые, и добавив свои хотелки я
составил примерно такой чеклист:
- [ ] Авторизация и общая доска лидерства
- [ ] После угадывания спрашивать у игрока «сложность», чтобы потом можно было, например, настраивать чтобы попадались
только простые объекты. И, например, разное количество очков за простые и сложные объекты
- [ ] Подумать как вынести игру в оффлайн, по типу того же ингресса. Это сложно и предстоит хорошо это обдумать
Как-то так :) А впереди новые выходные и новые «проекты выходного дня»!