Weil gemeinsame Arbeit zu meinem Vorteil
- sie verbindet.
Matroskin Wirf
Kieselsteine ins Wasser und betrachte die Kreise, die sie bilden. Andernfalls macht ein solches Werfen leeren Spaß.
Kozma Prutkov "Gedanken und Aphorismen".
Vor kurzem, letzten Freitag, haben wir beschlossen, unseren Alltag durch ein Programmierturnier leicht zu diversifizieren. Die Tagesordnung wurde nicht sofort festgelegt. Es gab Gedanken über analytische Datenverarbeitung und maschinelles Lernen, aber am Ende entschieden sie sich für Brettspiele. Wir wollten ein Element des Wettbewerbs in die Veranstaltung einführen, aber was, wenn nicht Spiele, macht dies einfach?

Das Team, das am Wettbewerb teilnehmen wollte, war verfügbar und hat auch den Preisfonds herausgefunden - es bleibt die Entscheidung über das Spiel. Ich schlug "Atari Go" vor und hatte die überzeugendsten Gründe dafür.
, '' ''?
- —
- — , « »
- « » , " "
- , , , "-"
- , ,
Ich sehe Einwände bis zum letzten Punkt voraus. Ja, in der Tat sind viele Bots für Go geschrieben und es ist überhaupt kein Problem, eine zugängliche Implementierung zu finden, aber Atari Go ist ein anderes Spiel. Der Verlust einzelner Steine in Go wird nicht als Katastrophe angesehen - die Ziele im Spiel sind völlig unterschiedlich. In Atari Go ist der Verlust von nur einer Klappe eine sofortige Niederlage.
Da wir die Teilnehmer nicht an eine Programmiersprache binden wollten, wurde beschlossen, einen Webdienst zu entwickeln, der eine REST-API bereitstelltum die Züge der Turnierteilnehmer zu registrieren. In der Folge hat sich diese Idee voll und ganz gerechtfertigt. Neben Java verwendeten die Konkurrenten C ++, Kotlin und sogar Lua als Entwicklungssprachen. Um die möglichen Auswirkungen unterschiedlicher Leistung von Computern auszuschließen, auf denen die Bots ausgeführt werden sollten, wurden zwei Sätze desselben Mini-PC- Typs gekauft und zunächst getestet , auf denen die 20. Version von Ubuntu Linux OS installiert war.

Der Game- Tracking- Service wurde in Node.js unter Verwendung des Nest- Frameworks entwickelt , aber das war nur die halbe Miete. Tatsache ist, dass der Server als universelle Lösung konzipiert wurde, die nicht von den Besonderheiten eines der Spiele abhängt. Seine Aufgabe ist es, die Züge der Spieler in der Datenbank aufzuzeichnen und die Zeit zu kontrollieren, aber sie überprüft die Züge selbst nicht auf Richtigkeit. Die Überprüfung der Richtigkeit der Züge sowie die Ermittlung des Gewinners ist Aufgabe des Arbiter , einer kleinen JavaScript-Anwendung, die über die jQuery- Bibliothek eine Verbindung zum Server herstellt .
Weitere technische Details
— , . PostgreSQL. « » , , :
user- token-, ( JWT-). games ( « » ). game_sessions. , ( ) user_games. game_moves.
, (POST api/session), (POST api/challenge) (POST api/move). (GET api/challenge) (POST api/join). , , , (GET api/move/confirmed/:id, id — ) (POST api/move).
, , , . , (games.main_time), (games.additional_time), . ( ), . . , , , .
( ), — , . — , (setup_str), , ( , , , ). , ( ). , ( ).

user- token-, ( JWT-). games ( « » ). game_sessions. , ( ) user_games. game_moves.
API
{
"openapi":"3.0.0",
"info":{
"title":"Dagaz Server",
"description":"Dagaz Server API description",
"version":"0.0.1",
"contact":{
}
},
"tags":[
{
"name":"dagaz",
"description":""
}
],
"servers":[
],
"components":{
"schemas":{
"User":{
"type":"object",
"properties":{
"id":{
"type":"number"
},
"is_admin":{
"type":"number"
},
"name":{
"type":"string"
},
"username":{
"type":"string"
},
"password":{
"type":"string"
},
"email":{
"type":"string"
},
"created":{
"format":"date-time",
"type":"string"
},
"deleted":{
"format":"date-time",
"type":"string"
},
"last_actived":{
"format":"date-time",
"type":"string"
}
},
"required":[
"id",
"name",
"username",
"created",
"last_actived"
]
},
"Pref":{
"type":"object",
"properties":{
"id":{
"type":"number"
},
"user_id":{
"type":"number"
},
"game_id":{
"type":"number"
},
"created":{
"format":"date-time",
"type":"string"
}
},
"required":[
"game_id"
]
},
"Sess":{
"type":"object",
"properties":{
"id":{
"type":"number"
},
"status":{
"type":"number"
},
"game_id":{
"type":"number"
},
"game":{
"type":"string"
},
"filename":{
"type":"string"
},
"created":{
"format":"date-time",
"type":"string"
},
"creator":{
"type":"string"
},
"changed":{
"format":"date-time",
"type":"string"
},
"closed":{
"format":"date-time",
"type":"string"
},
"players_total":{
"type":"number"
},
"winner":{
"type":"number"
},
"loser":{
"type":"number"
},
"score":{
"type":"number"
},
"last_setup":{
"type":"string"
}
},
"required":[
"game_id"
]
},
"Challenge":{
"type":"object",
"properties":{
"id":{
"type":"number"
},
"session_id":{
"type":"number"
},
"user_id":{
"type":"number"
},
"user":{
"type":"string"
},
"player_num":{
"type":"number"
}
},
"required":[
"session_id"
]
},
"Join":{
"type":"object",
"properties":{
"id":{
"type":"number"
},
"user_id":{
"type":"number"
},
"user":{
"type":"string"
},
"session_id":{
"type":"number"
},
"player_num":{
"type":"number"
},
"is_ai":{
"type":"number"
}
},
"required":[
"session_id"
]
},
"Move":{
"type":"object",
"properties":{
"id":{
"type":"number"
},
"session_id":{
"type":"number"
},
"user_id":{
"type":"number"
},
"turn_num":{
"type":"number"
},
"move_str":{
"type":"string"
},
"setup_str":{
"type":"string"
},
"note":{
"type":"string"
},
"time_delta":{
"type":"number"
},
"time_limit":{
"type":"number"
},
"additional_time":{
"type":"number"
}
},
"required":[
"session_id",
"user_id",
"move_str"
]
},
"Result":{
"type":"object",
"properties":{
"id":{
"type":"number"
},
"session_id":{
"type":"number"
},
"user_id":{
"type":"number"
},
"result_id":{
"type":"number"
},
"score":{
"type":"number"
}
},
"required":[
"session_id",
"result_id"
]
}
}
},
"paths":{
"/api/auth/login":{
"post":{
"operationId":"AppController_login",
"parameters":[
],
"requestBody":{
"required":true,
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/User"
}
}
}
}
},
"responses":{
"201":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
}
},
"security":[
{
"basic":[
]
}
]
}
},
"/api/auth/refresh":{
"get":{
"operationId":"AppController_refresh",
"parameters":[
],
"responses":{
"201":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
}
},
"security":[
{
"basic":[
]
}
]
}
},
"/api/users":{
"get":{
"operationId":"UsersController_findAll",
"parameters":[
],
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
},
"post":{
"operationId":"UsersController_update",
"parameters":[
],
"requestBody":{
"required":true,
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/User"
}
}
}
}
},
"responses":{
"201":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/users/{id}":{
"get":{
"operationId":"UsersController_findUsers",
"parameters":[
],
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
},
"delete":{
"operationId":"UsersController_delete",
"parameters":[
],
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"403":{
"description":"Forbidden."
},
"404":{
"description":"Not Found."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/preferences":{
"get":{
"operationId":"PreferencesController_findAll",
"parameters":[
],
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
},
"post":{
"operationId":"PreferencesController_create",
"parameters":[
],
"requestBody":{
"required":true,
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/Pref"
}
}
}
}
},
"responses":{
"201":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/preferences/{id}":{
"delete":{
"operationId":"PreferencesController_delete",
"parameters":[
],
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"404":{
"description":"Not Found."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/session":{
"get":{
"operationId":"SessionController_findAll",
"parameters":[
],
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"403":{
"description":"Forbidden."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
},
"post":{
"operationId":"SessionController_create",
"parameters":[
],
"requestBody":{
"required":true,
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/Sess"
}
}
}
}
},
"responses":{
"201":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/session/{id}":{
"get":{
"operationId":"SessionController_getSession",
"parameters":[
],
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"404":{
"description":"Not Found."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/session/close":{
"post":{
"operationId":"SessionController_close",
"parameters":[
],
"requestBody":{
"required":true,
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/Sess"
}
}
}
}
},
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"403":{
"description":"Forbidden."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/challenge":{
"get":{
"operationId":"ChallengeController_findAll",
"parameters":[
],
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
},
"post":{
"operationId":"ChallengeController_create",
"parameters":[
],
"requestBody":{
"required":true,
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/Challenge"
}
}
}
}
},
"responses":{
"201":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/challenge/{id}":{
"delete":{
"operationId":"ChallengeController_delete",
"parameters":[
],
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"404":{
"description":"Not Found."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/join/{id}":{
"get":{
"operationId":"JoinController_findJoined",
"parameters":[
],
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/join":{
"post":{
"operationId":"JoinController_join",
"parameters":[
],
"requestBody":{
"required":true,
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/Join"
}
}
}
}
},
"responses":{
"201":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"404":{
"description":"Not Found."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/move/all/{id}":{
"get":{
"operationId":"MoveController_getMoves",
"parameters":[
],
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"403":{
"description":"Forbidden."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/move/unconfirmed/{id}":{
"get":{
"operationId":"MoveController_getUnconfirmedMove",
"parameters":[
],
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"403":{
"description":"Forbidden."
},
"404":{
"description":"Not Found."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/move/confirmed/{id}":{
"get":{
"operationId":"MoveController_getConfirmedMove",
"parameters":[
],
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"404":{
"description":"Not Found."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/move":{
"post":{
"operationId":"MoveController_update",
"parameters":[
],
"requestBody":{
"required":true,
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/Move"
}
}
}
}
},
"responses":{
"201":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"404":{
"description":"Not Found."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/move/confirm":{
"post":{
"operationId":"MoveController_confirm",
"parameters":[
],
"requestBody":{
"required":true,
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/Move"
}
}
}
}
},
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"403":{
"description":"Forbidden."
},
"404":{
"description":"Not Found."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/result/{id}":{
"get":{
"operationId":"ResultController_getMoves",
"parameters":[
],
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"404":{
"description":"Not Found."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/result":{
"post":{
"operationId":"ResultController_join",
"parameters":[
],
"requestBody":{
"required":true,
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/Result"
}
}
}
}
},
"responses":{
"201":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"404":{
"description":"Not Found."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
},
"/api/game":{
"get":{
"operationId":"GameController_allGames",
"parameters":[
],
"responses":{
"200":{
"description":"Successfully."
},
"401":{
"description":"Unauthorized."
},
"500":{
"description":"Internal Server error."
}
},
"security":[
{
"bearer":[
]
}
]
}
}
}
}
, (POST api/session), (POST api/challenge) (POST api/move). (GET api/challenge) (POST api/join). , , , (GET api/move/confirmed/:id, id — ) (POST api/move).
, , , . , (games.main_time), (games.additional_time), . ( ), . . , , , .
( ), — , . — , (setup_str), , ( , , , ). , ( ). , ( ).
Die Entwicklung von Bots ist selbst für Atari Go schwierig. Die drei Tage, die den Teilnehmern zur Vorbereitung zugewiesen wurden, reichten nur aus, damit die Bots einfach arbeiteten. Darüber hinaus erwiesen sich die Mini-PCs, auf denen der Wettbewerb stattfand, als wesentlich weniger produktiv als die Arbeitsplätze, an denen das Debuggen durchgeführt wurde. All dies führte dazu, dass die Bots während des Turniers nicht mit besonderer Intelligenz glänzten, aber dennoch lustige Momente passierten.

Dies ist ein Beispiel für eine endgültige Position in einem der Turnierspiele. Der Bot-Kampf war interessant und heftig. Am Ende versuchte Weiß, den Gegner im Shich zu fangen, bemerkte aber nicht, dass Schwarz ihn beim nächsten Zug in die Atari- Position brachte . Der Bot von White hat einen Fehler gemacht, als er versuchte, die „Leiter“ fortzusetzen. Schwarz nutzte dies sofort aus - nahm einen Stein und beendete das Spiel.
All dies verdeutlicht die Art der Fehler, die die Turnierteilnehmer gemacht haben.
, , , . , , , . :
"" — . « », , «E6», . , , , — , «» , . «», , . .
, , : "", "" "". , , , . , , , . , , , .
, , , . , «».
"" — . « », , «E6», . , , , — , «» , . «», , . .
, , : "", "" "". , , , . , , , . , , , .
,
« », , . , :
, , 5x5. , , (, «» ). , . , 90, 180 270 , . . .
, . , heuristic. , . , « », , , , .
1000 ;
-----
?????
??B??
?B.??
?????
?????
, , 5x5. , , (, «» ). , . , 90, 180 270 , . . .
Dagaz.AI.Patterns.push({re: /.{7}B.{3}B0.{12}/, price: 1000});
Dagaz.AI.Patterns.push({re: /.{11}B0.{4}B.{7}/, price: 1000});
Dagaz.AI.Patterns.push({re: /.{12}0B.{3}B.{7}/, price: 1000});
Dagaz.AI.Patterns.push({re: /.{7}B.{4}0B.{11}/, price: 1000});
, . , heuristic. , . , « », , , , .
, , , . , «».
Trotzdem verlief die Qualifikationsphase des Turniers, in der jeder der Teilnehmer zwei Spiele mit allen Bewerbern (weiß und schwarz) bestritt, gut, und anhand der Anzahl der Siege ermittelten wir zwei Finalisten.

Außerdem wurden die Spiele bis zu drei Siegen fortgesetzt, wobei die Reihenfolge des ersten Zuges geändert wurde. Nachdem er mit einem Endstand von 3: 1 gewonnen hatte, nahm ein glücklicher (und drei Nächte lang nicht schlafender) Gewinner seinen Preis entgegen:

Applaudieren wir ihm!