terkwood / bugout Goto Github PK
View Code? Open in Web Editor NEWAI-driven, Multiplayer Go/Weiqi/Baduk for the web ππ€π¦β
Home Page: https://go.terkwood.farm
License: MIT License
AI-driven, Multiplayer Go/Weiqi/Baduk for the web ππ€π¦β
Home Page: https://go.terkwood.farm
License: MIT License
Advances #4
If you send a MakeMoveCmd but there is no gamestate available to join:
judge_1 | MAKE MOVE CMD 00b8d848 BLACK Coord(x=0, y=18)
judge_1 | Exception in thread "bugout-judge-3474fb5f-ef9f-4a9f-b82d-7234a7120d44-StreamThread-1" java.lang.IllegalArgumentException: Parameter specified as non-null is null: method Judge$process$valueJoiner$1.apply, parameter rightValue
judge_1 | at Judge$process$valueJoiner$1.apply(Judge.kt)
judge_1 | at Judge$process$valueJoiner$1.apply(Judge.kt:18)
judge_1 | at org.apache.kafka.streams.kstream.internals.KStreamKTableJoinProcessor.process(KStreamKTableJoinProcessor.java:73)
judge_1 | at org.apache.kafka.streams.processor.internals.ProcessorNode.process(ProcessorNode.java:117)
In Gateway's model, we have a serde "type" tag on KafkaCommands
. And in the Kafka Streams services, we have JSON type annotations to deal with that "type" field. We should drop the type tag on KafkaCommands and delete the JSON annotations in the KS services
// model.rs
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
pub enum KafkaCommands {
MakeMove(MakeMoveCommand),
ProvideHistory(ProvideHistoryCommand),
}
The run scripts in changelog, judge] and gateway should have configurable wait values sourced from .env
files. This will support operation in various sized environments (small cloud instances vs hefty developer machines)
# alter the sleep value here
./wait-for-it.sh kafka:9092 -s -- sleep 16
Keep sane defaults in place in case the .env
files aren't present.
In some cases, a client will disconnect, and then reconnect thinking that it's the wrong player's move. The Reconnected
event exposed by gateway already makes it clear which player should be expected to take a turn next. The client should honor that field in order to avoid deadlocking the game for both players.
Related: #48, #49, & Terkwood/Sabaki#11
We'd like to persist a client_id (not a session_id!) in the browser localstorage and use it across websocket sessions
Use a topic with data
gameId -> { reqId: abc, success: bool }
As the key/value pair for the stream
Need to make sure this works well for the frontend gateway component. Finish end to end POC without checking move correctness before this ticket
With game IDs being automatically provisioned by the gateway, you should be able to put White into a passive state and get moves routed to you correctly.
Only use compact UUID in the frontend and resolve in gateway. Don't pass IDs around to the kafka layer in that format.
There are a bunch of manually-programmed startup timings affecting the kafka-streams services, and gateway. For history-provider
and game-lobby
, they seem especially suspect. We've noticed both of those services failing to respond on startup, which can hang the system. π
One way to work around this is in Sabaki -- for the "provide history" step in the web UI, specifically, we could give it only so much time to respond, before assuming there's no history available.
The game lobby has less leeway -- we can't run the system at all without the lobby functioning. So it would be best if we can ensure sane startup for all JVM services without needing the UI to work around them.
On connection from frontend to gateway, query a Ktable to see if there's an existing mapping from GCP UID to a gameId
Do not introduce an "internal user ID". This would add unnecessary complexity to the app
You either need to use a simple consumer/producer data flow
.... Or implement an additional JVM service behind the gateway to handle such a query using Kafka streams
If a newly-connected player sends a move prior to Sabaki having established a game ID by talking to gateway, you'll see an "undefined" dialog box and the move will never be communicated. In such a state, we should queue up the move so that it's sent after the connection to gateway is made
Reproducing this issue:
Connect to UI & pick black, and quickly place a piece. You'll see a dialog box with an OK button and "undefined" text
Entries are added but never removed -- fix this
Implement a system which allows playing against a KataGo instance running on an NVIDIA Jetson Nano.
We picked up an NVIDIA Jetson Nano board and built KataGo. Documentation is available in the repo now
listenForMove
when human plays whiteUse KataGo analysis engine to respond to queries.
When attempting to use the Pass control in Sabaki, Gateway received a garbled message with an x
coord but no y
π₯ 1589668e b95307e4 ERROR message deserialization {"type":"MakeMove","gameId":"b95307e4-a817-4917-8bbe-9291d01c9629","reqId":"2b3d0548-3c7d-4834-b285-aa5de6b4c613","player":"BLACK","coord":{"x":14,"y":null}}
Add a caddy reverse proxy to serve gateway traffic to the public internet
Advances #3
If two players move in quick succession, the gamestates aggregator(GSA) may fail to catch up.
We should make the gateway aware of the current GSA turn entry, and force it to queue any moves sent before the new turn is announced by GSA
Advances #4
In the event that someone creates a game, and then disconnects before an opponent joins, gateway must signal to kafka that the game should be removed from lobby. Use clientIDs to join, here
Child of #42
Aggregate MoveMadeEv
into a history-focused KTable, and then announce that result to the gateway in a dedicated topic.
This approach lets us separate the game state querying logic into GameBoard
and the replay logic into GameHistory
class GameHistory {
fun add(moveMadeEv: MoveMadeEv): GameHistory {
TODO()
return this
}
}
moveMadeEvStream
.aggregate(GameHistory(), { history -> history.add(move) },
Materialized.as("bugout-game-history-store))
.toStream()
.to("bugout-game-replay-ev")
The setAppElement example on codepen worked well enough
System isn't smart enough to recover from the state where your browser thinks it's WHITE to move, the server thinks it's WHITE to move, but MY browser thinks it's BLACK to move
It's fast and saves lots of space
https://cwiki.apache.org/confluence/display/KAFKA/KIP-110%3A+Add+Codec+for+ZStandard+Compression
See compresstion.type config
Child of #42
Related to #42
Gateway should listen for updates to streams based on gameId, and route them to appropriate clients.
Use a hard-coded gameId for the first draft
Let's run BUGOUT at a very low cost, relying mostly on a micro instance to support gateway and the reverse proxy, and starting/stopping the larger JVM services as necessary.
Big box starts up cleanly on a t2.medium. Runs Kafka and JVM apps. Reaper is implemented. Need to add startup service (bugle) π―
Provides low AWS cost, uses a single micro instance at all times.
enum RemoteControlCommands{ Off, On }
POST /rc/on
commandbugout-make-move-cmd
and bugout-game-states
MakeMoveCmd
, query game state via KTable. If move is allowed, write MoveMadeEv
(topic=bugout-move-made-ev). Use Kafka streams branch operatorbugout-move-made-ev
into the queryable KTable on bugout-game-states
This issue appears to be caused by BUGOUT Sabaki spamming a bunch of commands in a row.
Do we need to add a commands
array to the GTP Controller shim, and to the WebSocketController? This type of array is present in the node.js GTP module -- see source code
If you trigger a modal dialog in the UI, then the other player triggers a move, the MoveMade event is missed by the UI. This can result in having your player send the wrong color of move. Might be hard to reproduce, but if you see this sort of output in the gateway
component...
π€ 92d8dc09 BEEP
π 3dc0069d PING PONG (273.152ms)
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 4, y: 14 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 11, y: 4 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 16, y: 15 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 16, y: 14 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 5, y: 5 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 15, y: 15 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 10, y: 10 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 17, y: 15 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 16, y: 16 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 16, y: 17 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 12, y: 16 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 15, y: 16 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 16, y: 12 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 17, y: 16 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 14, y: 6 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 13, y: 12 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 8, y: 13 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 14, y: 7 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 16, y: 5 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 14, y: 5 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 15, y: 6 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 6, y: 8 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 5, y: 8 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 8, y: 8 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 8, y: 6 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 13, y: 9 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 13, y: 6 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 17, y: 14 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 18, y: 13 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 15, y: 14 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 17, y: 13 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 17, y: 12 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 17, y: 11 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 18, y: 11 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 18, y: 12 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 18, y: 10 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 17, y: 10 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 17, y: 9 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 18, y: 9 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 4, y: 8 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 5, y: 7 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 9, y: 3 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 9, y: 4 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 13, y: 11 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 14, y: 11 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 14, y: 12 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 15, y: 11 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 16, y: 11 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 16, y: 10 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 15, y: 12 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 16, y: 13 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 15, y: 10 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 14, y: 10 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 14, y: 9 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 15, y: 9 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 13, y: 10 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 17, y: 8 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 9, y: 9 })
β 92d8dc09 MOVE a68c21bb WHITE Some(Coord { x: 8, y: 10 })
β 92d8dc09 MOVE a68c21bb BLACK Some(Coord { x: 11, y: 13 })
π 92d8dc09 PING PONG (87.259ms)
Multiplayer Sabaki shows an ugly alert()
dialog when you try to join an invalid private game. Use the material dialog component to reskin this.
Move colorization can do a bit of blinking on some devices. Default to off
.... So that data for individual games is tracked correctly . Change existing GameBoard
class so that it accounts for this mapping. "AllGameBoards"?
With the POC finished, we need to add four data paths to support a full featured system:
enum Visibility { Public, Private}
struct OpenGame {
gameId: GameId, visibility: Visibility
}
We'll need a GameLobby to keep track of all games, public and private, in a global KTable
Whenever a game is joined, it's removed from that list π
Gateway tends to fall over in a light breeze. Make sure it can handle unexpected inputs
Gateway can help support this.
Instead of "AddClient" struct being processed by the router component, expose "RequestOpenGame" and a "Reconnect To Game" variants
When black makes a move before white connects, white will never hear about turn one
This exception was seen in multiple services.
Gateway service showed no errors at all
π οΈd026ae80 HISTPROV HistoryProvided(gameId=d026ae80-a7e1-40a3-8e02-d297bf611d5e, replyTo=d8469f2b-9238-45af-94e0-7b23004fbf0d, eventId=425fbe0e-f9c4-4e98-8b30-a233feac61b0, moves=[], epochMillis=1566684125177)
Exception in thread "bugout-history-provider-48398a94-7ebb-4572-98cd-3958b1e152a8-StreamThread-1" org.apache.kafka.streams.errors.StreamsException: task [0_0] Abort sending since an error caught with a previous record (key d026ae80-a7e1-40a3-8e02-d297bf611d5e value {"type":"HistoryProvided","gameId":"d026ae80-a7e1-40a3-8e02-d297bf611d5e","replyTo":"d8469f2b-9238-45af-94e0-7b23004fbf0d","eventId":"425fbe0e-f9c4-4e98-8b30-a233feac61b0","moves":[],"epochMillis":1566684125177} timestamp 1566684124980) to topic bugout-history-provided-ev due to org.apache.kafka.common.KafkaException: org.apache.kafka.common.errors.InvalidPidMappingException: The producer attempted to use a producer id which is not currently assigned to its transactional id.
at org.apache.kafka.streams.processor.internals.RecordCollectorImpl.recordSendError(RecordCollectorImpl.java:138)
at org.apache.kafka.streams.processor.internals.RecordCollectorImpl.access$500(RecordCollectorImpl.java:50)
at org.apache.kafka.streams.processor.internals.RecordCollectorImpl$1.onCompletion(RecordCollectorImpl.java:201)
at org.apache.kafka.clients.producer.KafkaProducer$InterceptorCallback.onCompletion(KafkaProducer.java:1318)
at org.apache.kafka.clients.producer.internals.ProducerBatch.completeFutureAndFireCallbacks(ProducerBatch.java:230)
at org.apache.kafka.clients.producer.internals.ProducerBatch.abort(ProducerBatch.java:158)
at org.apache.kafka.clients.producer.internals.RecordAccumulator.abortBatches(RecordAccumulator.java:742)
at org.apache.kafka.clients.producer.internals.Sender.maybeAbortBatches(Sender.java:479)
at org.apache.kafka.clients.producer.internals.Sender.runOnce(Sender.java:316)
at org.apache.kafka.clients.producer.internals.Sender.run(Sender.java:238)
at java.lang.Thread.run(Thread.java:748)
Caused by: org.apache.kafka.common.KafkaException: org.apache.kafka.common.errors.InvalidPidMappingException: The producer attempted to use a producer id which is not currently assigned to its transactional id.
at org.apache.kafka.clients.producer.internals.TransactionManager$AddPartitionsToTxnHandler.handleResponse(TransactionManager.java:1204)
at org.apache.kafka.clients.producer.internals.TransactionManager$TxnRequestHandler.onComplete(TransactionManager.java:1069)
at org.apache.kafka.clients.ClientResponse.onComplete(ClientResponse.java:109)
at org.apache.kafka.clients.NetworkClient.completeResponses(NetworkClient.java:561)
at org.apache.kafka.clients.NetworkClient.poll(NetworkClient.java:553)
at org.apache.kafka.clients.producer.internals.Sender.runOnce(Sender.java:307)
... 2 more
Caused by: org.apache.kafka.common.errors.InvalidPidMappingException: The producer attempted to use a producer id which is not currently assigned to its transactional id.
βοΈ οΈd026ae80 ACCEPT BLACK @ Coord(x=14, y=13) capturing
Exception in thread "bugout-judge-17432a73-0993-4717-b94d-ba7218e9e859-StreamThread-1" org.apache.kafka.streams.errors.StreamsException: task [0_0] Abort sending since an error caught with a previous record (key d026ae80-a7e1-40a3-8e02-d297bf611d5e value {"type":"MoveMade","gameId":"d026ae80-a7e1-40a3-8e02-d297bf611d5e","replyTo":"bc148b87-04f0-49f0-bfc5-d135d3453bb6","eventId":"f7d635d9-f7c1-4602-bf37-ab375f4dd266","player":"BLACK","coord":{"x":14,"y":13},"captured":[]} timestamp 1566684102451) to topic bugout-move-accepted-ev due to org.apache.kafka.common.KafkaException: org.apache.kafka.common.errors.InvalidPidMappingException: The producer attempted to use a producer id which is not currently assigned to its transactional id.
at org.apache.kafka.streams.processor.internals.RecordCollectorImpl.recordSendError(RecordCollectorImpl.java:138)
at org.apache.kafka.streams.processor.internals.RecordCollectorImpl.access$500(RecordCollectorImpl.java:50)
at org.apache.kafka.streams.processor.internals.RecordCollectorImpl$1.onCompletion(RecordCollectorImpl.java:201)
at org.apache.kafka.clients.producer.KafkaProducer$InterceptorCallback.onCompletion(KafkaProducer.java:1310)
at org.apache.kafka.clients.producer.internals.ProducerBatch.completeFutureAndFireCallbacks(ProducerBatch.java:230)
at org.apache.kafka.clients.producer.internals.ProducerBatch.abort(ProducerBatch.java:158)
at org.apache.kafka.clients.producer.internals.RecordAccumulator.abortBatches(RecordAccumulator.java:742)
at org.apache.kafka.clients.producer.internals.Sender.maybeAbortBatches(Sender.java:460)
at org.apache.kafka.clients.producer.internals.Sender.runOnce(Sender.java:297)
at org.apache.kafka.clients.producer.internals.Sender.run(Sender.java:235)
at java.lang.Thread.run(Thread.java:748)
Caused by: org.apache.kafka.common.KafkaException: org.apache.kafka.common.errors.InvalidPidMappingException: The producer attempted to use a producer id which is not currently assigned to its transactional id.
at org.apache.kafka.clients.producer.internals.TransactionManager$AddPartitionsToTxnHandler.handleResponse(TransactionManager.java:1041)
at org.apache.kafka.clients.producer.internals.TransactionManager$TxnRequestHandler.onComplete(TransactionManager.java:909)
at org.apache.kafka.clients.ClientResponse.onComplete(ClientResponse.java:109)
at org.apache.kafka.clients.NetworkClient.completeResponses(NetworkClient.java:557)
at org.apache.kafka.clients.NetworkClient.poll(NetworkClient.java:549)
at org.apache.kafka.clients.producer.internals.Sender.runOnce(Sender.java:288)
... 2 more
Caused by: org.apache.kafka.common.errors.InvalidPidMappingException: The producer attempted to use a producer id which is not currently assigned to its transactional id.
Presently, you have to click the little black/white piece selector in PlayBar
to change sides, even if you choose to play as white when prompted by the dialog
Use Sabaki
Wait for opponent doesn't come when the initial find public game command is made. It comes at the same time as the Game Ready event
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.