From e52910fa1933db903b039a0f4f32b6470b3100d4 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Sat, 6 Jun 2020 08:55:23 -0700 Subject: [PATCH] Initial implementation of Scribe This is not at all sophisticated yet. Some of the things that are missing: * Proper layout of the board (need spacing to separate mini grids from each other). * Move restrictions: Don't allow a player to move in a super grid that doesn't correspond to their last move's mini-grid placement (unless the corresponding super-grid is full). * Presentation to the user of the scored glyph shapes * Scoring of completed mini grids * Scoring of the super grids for the final game And on that last point, there needs to be an option to play either the "majority" or "super-glyph" variation for the final scoring. --- scribe/.gitignore | 1 + scribe/Makefile | 12 ++ scribe/game.html | 35 ++++ scribe/index.html | 38 ++++ scribe/scribe.css | 50 +++++ scribe/scribe.jsx | 494 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 630 insertions(+) create mode 100644 scribe/.gitignore create mode 100644 scribe/Makefile create mode 100644 scribe/game.html create mode 100644 scribe/index.html create mode 100644 scribe/scribe.css create mode 100644 scribe/scribe.jsx diff --git a/scribe/.gitignore b/scribe/.gitignore new file mode 100644 index 0000000..953891d --- /dev/null +++ b/scribe/.gitignore @@ -0,0 +1 @@ +scribe.js diff --git a/scribe/Makefile b/scribe/Makefile new file mode 100644 index 0000000..9f07401 --- /dev/null +++ b/scribe/Makefile @@ -0,0 +1,12 @@ +# Defer all targets up to the upper-level +# +# This requires two recipes. The first to cover the case of no +# explicit target specifed (so when invoked as "make" we call "make" +# at the upper-level) and then a .DEFAULT recipe to pass any explicit +# target up as well, (so that an invocation of "make foo" results in a +# call to "make foo" above. +all: + $(MAKE) -C .. + +.DEFAULT: + $(MAKE) -C .. $@ diff --git a/scribe/game.html b/scribe/game.html new file mode 100644 index 0000000..b9136b7 --- /dev/null +++ b/scribe/game.html @@ -0,0 +1,35 @@ + + + + + + + Tic-tac-toe + + + + + + + + + + + +
+ +

Tic Tac Toe

+ +

+ Just the classic game. +

+ +
+
+ +
+ +
+ + + diff --git a/scribe/index.html b/scribe/index.html new file mode 100644 index 0000000..7638db9 --- /dev/null +++ b/scribe/index.html @@ -0,0 +1,38 @@ + + + + + + + Scribe + + + + + + + + +
+ +

Scribe

+ +

+ A game + by Mark + Steere, implemented by permission. +

+ +
+
+ +
+ +
+ +
+ + + diff --git a/scribe/scribe.css b/scribe/scribe.css new file mode 100644 index 0000000..4eaea8f --- /dev/null +++ b/scribe/scribe.css @@ -0,0 +1,50 @@ +ol, ul { + padding-left: 30px; +} + +.board-row:after { + clear: both; + content: ""; + display: table; +} + +.status { + margin-bottom: 10px; +} + +.square { + background: #fff; + color: black; + border: 1px solid #999; + float: left; + font-size: 20px; + font-weight: bold; + line-height: 25px; + width: 25px; + height: 25px; + margin-right: -1px; + margin-top: -1px; + padding: 0; + text-align: center; + border-radius: 4px; +} + +.square.open { + cursor: pointer; +} + +.square.occupied { + cursor: default; +} + +.square.open:hover { + background-color: var(--accent-color-bright); +} + +.square:focus { + outline: none; +} + +.kbd-navigation .square:focus { + background: #ddd; +} diff --git a/scribe/scribe.jsx b/scribe/scribe.jsx new file mode 100644 index 0000000..e7654b4 --- /dev/null +++ b/scribe/scribe.jsx @@ -0,0 +1,494 @@ +function team_symbol(team) { + if (team === "+") + return "🞥"; + else + return "🞇"; +} + +function undisplay(element) { + element.style.display="none"; +} + +function add_message(severity, message) { + message = `
+× +${message} +
`; + const message_area = document.getElementById('message-area'); + message_area.insertAdjacentHTML('beforeend', message); +} + +/********************************************************* + * Handling server-sent event stream * + *********************************************************/ + +const events = new EventSource("events"); + +events.onerror = function(event) { + if (event.target.readyState === EventSource.CLOSED) { + add_message("danger", "Connection to server lost."); + } +}; + +events.addEventListener("game-info", event => { + const info = JSON.parse(event.data); + + window.game.set_game_info(info); +}); + +events.addEventListener("player-info", event => { + const info = JSON.parse(event.data); + + window.game.set_player_info(info); +}); + +events.addEventListener("player-enter", event => { + const info = JSON.parse(event.data); + + window.game.set_other_player_info(info); +}); + +events.addEventListener("player-update", event => { + const info = JSON.parse(event.data); + + if (info.id === window.game.state.player_info.id) + window.game.set_player_info(info); + else + window.game.set_other_player_info(info); +}); + +events.addEventListener("move", event => { + const move = JSON.parse(event.data); + + window.game.receive_move(move); +}); + +events.addEventListener("game-state", event => { + const state = JSON.parse(event.data); + + window.game.reset_board(); + + for (let square of state.moves) { + window.game.receive_move(square); + } +}); + +/********************************************************* + * Game and supporting classes * + *********************************************************/ + +function GameInfo(props) { + if (! props.id) + return null; + + return ( +
+

{props.id}

+ Invite a friend to play by sending this URL: {props.url} +
+ ); +} + +function TeamButton(props) { + return ; +} + +function TeamChoices(props) { + let other_team; + if (props.player.team === "+") + other_team = "o"; + else + other_team = "+"; + + if (props.player.team === "") { + if (props.first_move) { + return null; + } else { + return [ + , + " ", + + ]; + } + } else { + return ; + } +} + +function PlayerInfo(props) { + if (! props.player.id) + return null; + + const choices = ; + + return ( +
+

Players

+ {props.player.name} + {props.player.team ? ` (${props.player.team})` : ""} + {props.first_move ? "" : " "} + {choices} + {props.other_players.map(other => ( + + {", "} + {other.name} + {other.team ? ` (${other.team})` : ""} + + ))} +
+ ); +} + +function Square(props) { + let className = "square"; + + if (props.value) { + className += " occupied"; + } else if (props.active) { + className += " open"; + } + + const onClick = props.active ? props.onClick : null; + + return ( +
+ {props.value} +
+ ); +} + +class Board extends React.Component { + render_square(i,j) { + const value = this.props.squares[i][j]; + return ( + this.props.onClick(i,j)} + /> + ); + } + + render() { + return ( +
+
+ {this.render_square(0,0)} + {this.render_square(0,1)} + {this.render_square(0,2)} + {" "} + {this.render_square(1,0)} + {this.render_square(1,1)} + {this.render_square(1,2)} + {" "} + {this.render_square(2,0)} + {this.render_square(2,1)} + {this.render_square(2,2)} +
+
+ {this.render_square(0,3)} + {this.render_square(0,4)} + {this.render_square(0,5)} + {" "} + {this.render_square(1,3)} + {this.render_square(1,4)} + {this.render_square(1,5)} + {" "} + {this.render_square(2,3)} + {this.render_square(2,4)} + {this.render_square(2,5)} +
+
+ {this.render_square(0,6)} + {this.render_square(0,7)} + {this.render_square(0,8)} + {" "} + {this.render_square(1,6)} + {this.render_square(1,7)} + {this.render_square(1,8)} + {" "} + {this.render_square(2,6)} + {this.render_square(2,7)} + {this.render_square(2,8)} +
+ +
+
+ +
+ {this.render_square(3,0)} + {this.render_square(3,1)} + {this.render_square(3,2)} + {" "} + {this.render_square(4,0)} + {this.render_square(4,1)} + {this.render_square(4,2)} + {" "} + {this.render_square(5,0)} + {this.render_square(5,1)} + {this.render_square(5,2)} +
+
+ {this.render_square(3,3)} + {this.render_square(3,4)} + {this.render_square(3,5)} + {" "} + {this.render_square(4,3)} + {this.render_square(4,4)} + {this.render_square(4,5)} + {" "} + {this.render_square(5,3)} + {this.render_square(5,4)} + {this.render_square(5,5)} +
+
+ {this.render_square(3,6)} + {this.render_square(3,7)} + {this.render_square(3,8)} + {" "} + {this.render_square(4,6)} + {this.render_square(4,7)} + {this.render_square(4,8)} + {" "} + {this.render_square(5,6)} + {this.render_square(5,7)} + {this.render_square(5,8)} +
+ +
+
+ +
+ {this.render_square(6,0)} + {this.render_square(6,1)} + {this.render_square(6,2)} + {" "} + {this.render_square(7,0)} + {this.render_square(7,1)} + {this.render_square(7,2)} + {" "} + {this.render_square(8,0)} + {this.render_square(8,1)} + {this.render_square(8,2)} +
+
+ {this.render_square(6,3)} + {this.render_square(6,4)} + {this.render_square(6,5)} + {" "} + {this.render_square(7,3)} + {this.render_square(7,4)} + {this.render_square(7,5)} + {" "} + {this.render_square(8,3)} + {this.render_square(8,4)} + {this.render_square(8,5)} +
+
+ {this.render_square(6,6)} + {this.render_square(6,7)} + {this.render_square(6,8)} + {" "} + {this.render_square(7,6)} + {this.render_square(7,7)} + {this.render_square(7,8)} + {" "} + {this.render_square(8,6)} + {this.render_square(8,7)} + {this.render_square(8,8)} +
+ +
+ ); + } +} + +function fetch_method_json(method, api = '', data = {}) { + const response = fetch(api, { + method: method, + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + return response; +} + +function fetch_post_json(api = '', data = {}) { + return fetch_method_json('POST', api, data); +} + +async function fetch_put_json(api = '', data = {}) { + return fetch_method_json('PUT', api, data); +} + +class Game extends React.Component { + constructor(props) { + super(props); + this.state = { + game_info: {}, + player_info: {}, + other_players: [], + squares: Array(9).fill(null).map(() => Array(9).fill(null)), + moves: 0, + next_to_play: "+" + }; + } + + set_game_info(info) { + this.setState({ + game_info: info + }); + } + + set_player_info(info) { + this.setState({ + player_info: info + }); + } + + set_other_player_info(info) { + const other_players_copy = [...this.state.other_players]; + const idx = other_players_copy.findIndex(o => o.id === info.id); + if (idx >= 0) { + other_players_copy[idx] = info; + } else { + other_players_copy.push(info); + } + this.setState({ + other_players: other_players_copy + }); + } + + reset_board() { + this.setState({ + next_to_play: "+" + }); + } + + receive_move(move) { + if (this.state.moves === 81) { + return; + } + const symbol = team_symbol(this.state.next_to_play); + const new_squares = this.state.squares.map(arr => arr.slice()); + new_squares[move[0]][move[1]] = symbol; + let next_to_play; + if (this.state.next_to_play === "+") + next_to_play = "o"; + else + next_to_play = "+"; + this.setState({ + squares: new_squares, + moves: this.state.moves + 1, + next_to_play: next_to_play + }); + } + + async handle_click(i, j, first_move) { + let move = { + move: [i, j] + }; + if (first_move) { + move.assert_first_move = true; + } + const response = await fetch_post_json("move", move); + if (response.status == 200) { + const result = await response.json(); + if (! result.legal) + add_message("danger", result.message); + } else { + add_message("danger", `Error occurred sending move`); + } + } + + join_team(team) { + fetch_put_json("player", {team: team}); + } + + render() { + const state = this.state; + const first_move = state.moves === 0; + const my_team = state.player_info.team; + var board_active; + + let status; + if (this.state.moves.length === 81) + { + status = "Game over"; + board_active = false; + } + else if (first_move) + { + if (state.other_players.length == 0) { + status = "You can move or wait for another player to join."; + } else { + let qualifier; + if (state.other_players.length == 1) { + qualifier = "Either"; + } else { + qualifier = "Any"; + } + status = `${qualifier} player can make the first move.`; + } + board_active = true; + } + else if (my_team === "") + { + status = "You're just watching the game."; + board_active = false; + } + else if (my_team === state.next_to_play) + { + status = "Your turn. Make a move."; + board_active = true; + } + else + { + status = "Waiting for another player to "; + if (state.other_players.length == 0) { + status += "join."; + } else { + status += "move."; + } + board_active = false; + } + + return [ + , + , +
+
{status}
+
+ this.handle_click(i, j, first_move)} + /> +
+
+ ]; + } +} + +ReactDOM.render( window.game = me} + />, document.getElementById("scribe")); -- 2.43.0