共同作業なので、私の利益のために
-それは団結します。
マトロスキン
小石を水に投げ込み、それらが形成する円を見てください。そうでなければ、そのような投げは空の楽しみになります。
コズマ・プルトコフ「思考と嫌悪」。
最近、先週の金曜日に、プログラミングトーナメントを開催して、日常生活を少し多様化することにしました。議題はすぐには決定されませんでした。分析データの処理、機械学習についての考えがありましたが、結局、彼らはボードゲームに落ち着きました。イベントに競争の要素を導入したかったのですが、ゲームではないにしても、これを簡単にするものは何ですか?

それで、競争に参加したいチームが利用可能でした、彼らはまた賞金を考え出しました-それはゲームを決定するために残っています。私は「アタリ囲碁」を提案しましたが、その理由は最も説得力がありました。
, '' ''?
- —
- — , « »
- « » , " "
- , , , "-"
- , ,
私は最後の点への反対を予見します。はい、確かに、多くのボットはGo用に作成されており、アクセス可能な実装を見つけることはまったく問題ではありませんが、AtariGoは別のゲームです。 Goで個々の石を失ったことは災害とは見なされません。ゲームの目標は完全に異なります。アタリ囲碁では、1石でも失ったことは即敗です。
参加者を1つのプログラミング言語にバインドしたくなかったため、RESTAPIを提供するWebサービスを開発することにしました。トーナメント参加者の動きを登録します。その後、このアイデアは完全に正当化されました。 Javaに加えて、競合他社は開発言語としてC ++、Kotlin、さらにはLuaを使用していました。ボットの実行が計画されているコンピューターのパフォーマンスの違いによる影響を排除するために、同じタイプのミニPCを2セット購入して最初にテストし、Ubuntu Linux OS20バージョンをインストールしました。

ゲーム追跡サービスは、Nestフレームワークを使用してNode.jsで開発されましたが、それは戦いの半分にすぎませんでした。事実、サーバーはどのゲームの詳細にも依存しないユニバーサルソリューションとして考案されました。そのタスクは、プレーヤーの動きをデータベースに記録し、時間を制御することですが、動き自体が正しいかどうかはチェックしません。移動の正しさを確認し、勝者を決定するのは、jQueryライブラリを使用してサーバーに接続する小さなJavaScriptアプリケーションであるArbiterのタスクです。
より技術的な詳細
— , . 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), , ( , , , ). , ( ). , ( ).
Atari Goであっても、ボットの開発は困難です。準備のために出場者に割り当てられた3日間は、ボットが単に機能するためだけに十分でした。さらに、コンテストが開催されたミニPCは、デバッグが実行された職場よりも生産性が大幅に低いことが判明しました。これらすべてが、トーナメント中のボットが特別な知性で輝かなかったという事実につながりましたが、それでも面白い瞬間が起こりました。

これは、トーナメントゲームの1つでの最終的な位置の例です。ボットの戦いは面白くて激しいものでした。結局、ホワイトはシチョで相手を捕まえようとしたが、次の動きでブラックがアタリの位置にいることに気づかなかった。ホワイトのボットは、「はしご」を続けようとしてミスを犯しました。黒はすぐにこれを利用しました-石を取り、ゲームを終了しました。
これはすべて、トーナメント参加者が犯した間違いの性質をよく表しています。
, , , . , , , . :
"" — . « », , «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. , . , « », , , , .
, , , . , «».
それでも、参加者全員が応募者全員(白と黒)で2試合を戦うトーナメントの予選ステージは順調に進み、勝利数で2人のファイナリストを決定しました。

さらに、ゲームは3回の勝利まで続き、最初の動きの順序が入れ替わりました。最終スコア3:1で勝ったので、満足した(そして3泊しなかった)勝者は彼の賞を取りました:

彼に拍手を送りましょう!