1
// This file is part of hnefatafl-copenhagen.
2
//
3
// hnefatafl-copenhagen is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU Affero General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// hnefatafl-copenhagen is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU Affero General Public License for more details.
12
//
13
// You should have received a copy of the GNU Affero General Public License
14
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
//
16
// SPDX-License-Identifier: AGPL-3.0-or-later
17
// SPDX-FileCopyrightText: 2025 David Campbell <david@hnefatafl.org>
18

            
19
use std::str::FromStr;
20

            
21
use anyhow::Context;
22

            
23
use crate::{
24
    play::{Plae, Vertex},
25
    role::Role,
26
    time,
27
};
28

            
29
/// hnefatafl-text-protocol binary and javascript pkg
30
///
31
/// All of the positions are lowercase for 11x11 and uppercase for 13x13.
32
///
33
/// The engine is line orientated and takes UTF-8 encoded text. The engine takes the below commands
34
/// and returns `= response` on success and `? error_message` on failure. If only comments and
35
/// whitespace or the empty string are passed the engine ignores the input and requests another
36
/// string. Comments are any text following `#` on a line.
37
///
38
/// Valid **ROLE** strings are `a`, `attacker`, `d`, and `defender`. Case does not matter.
39
///
40
/// Valid **TO** and **FROM** coordinates are a letter, uppercase or lowercase, `A` through `M`
41
/// followed by a number `1` through `13`. For example, `A1`.
42
///
43
/// **MILLISECONDS** and **ADD_SECONDS** are numbers.
44
///
45
/// In order to run the javascript pkg:
46
///
47
/// ```sh
48
/// cargo install wasm-pack
49
/// make js
50
/// ```
51
///
52
/// Then copy the pkg folder to your web browser's site folder. For example with Apache on Debian:
53
///
54
/// ```sh
55
/// sudo mkdir --parent /var/www/html/pkg
56
/// sudo cp -r pkg /var/www/html
57
/// ```
58
///
59
/// Or if you installed the package via npm:
60
///
61
/// ```sh
62
/// sudo mkdir --parent /var/www/html/pkg
63
/// sudo cp ~/node_modules/hnefatafl-copenhagen/* /var/www/html/pkg
64
/// ```
65
///
66
/// Then load the javascript on a webpage:
67
///
68
/// ```sh
69
/// cat << EOF > /var/www/html/index.html
70
/// <!DOCTYPE html>
71
/// <html>
72
/// <head>
73
///     <title>Copenhagen Hnefatafl</title>
74
/// </head>
75
/// <body>
76
///     <h1>Copenhagen Hnefatafl</h1>
77
///     <script type="module">
78
///         import init, { Game } from '../pkg/hnefatafl_copenhagen.js';
79
///
80
///         init().then(() => {
81
///             const game = new Game();
82
///             const output = game.read_line_js("show_board");
83
///             console.log(output);
84
///         });
85
///     </script>
86
/// </body>
87
/// </html>
88
/// EOF
89
/// ```
90
#[allow(clippy::doc_markdown)]
91
#[derive(Debug, Clone)]
92
pub enum Message {
93
    /// `board_size` 11 | `board_size` 13
94
    ///
95
    ///  Sets the game to initial conditions with board size 11 or 13.
96
    BoardSize(usize),
97

            
98
    /// The empty string or only comments and whitespace was passed.
99
    Empty,
100

            
101
    /// `final_status`
102
    ///
103
    /// Returns `attacker_wins` or `draw` or `ongoing` or `defender_wins`.
104
    FinalStatus,
105

            
106
    /// `generate_move`
107
    ///
108
    /// Returns `play ROLE FROM TO`.
109
    GenerateMove,
110

            
111
    /// `known_command STRING`
112
    ///
113
    /// Returns a boolean signifying whether the engine knows the command.
114
    KnownCommand(String),
115

            
116
    /// `list_commands`
117
    ///
118
    /// Lists all of the known commands, each separated by a newline.
119
    ListCommands,
120

            
121
    /// `name`
122
    ///
123
    /// Prints the name of the package.
124
    Name,
125

            
126
    /// `play ROLE FROM TO` | `play ROLE resign`
127
    ///
128
    /// Plays a move and returns **CAPTURES**, where **CAPTURES** has the format `A2 C2 ...`.
129
    Play(Plae),
130

            
131
    /// `play_from`
132
    ///
133
    /// Returns a **ROLE** followed by all the valid **FROM** squares.
134
    PlayFrom,
135

            
136
    /// `play_to ROLE FROM`
137
    ///
138
    /// Returns all the valid **TO** squares.
139
    PlayTo((Role, Vertex)),
140

            
141
    /// `protocol_version`
142
    ///
143
    /// Prints the version of the Hnefatafl Text Protocol.
144
    ProtocolVersion,
145

            
146
    /// `quit`
147
    ///
148
    /// quits the engine.
149
    Quit,
150

            
151
    /// `show_board`
152
    ///
153
    /// Displays the board
154
    ShowBoard,
155

            
156
    /// `time_settings un-timed` | `time_settings fischer MILLISECONDS ADD_SECONDS`
157
    ///
158
    /// Choose the time settings and reset the plays. For fischer time **MILLISECONDS** is the starting time and
159
    /// **ADD_SECONDS** is how much time to add after each move. **ADD_SECONDS** may be zero, in
160
    /// which case the time settings are really absolute time.
161
    TimeSettings(time::TimeSettings),
162

            
163
    /// `version`
164
    ///
165
    /// Displays the package version.
166
    Version,
167
}
168

            
169
pub static COMMANDS: [&str; 14] = [
170
    "board_size",
171
    "final_status",
172
    "generate_move",
173
    "known_command",
174
    "list_commands",
175
    "name",
176
    "play",
177
    "play_from",
178
    "play_to",
179
    "protocol_version",
180
    "quit",
181
    "show_board",
182
    "time_settings",
183
    "version",
184
];
185

            
186
impl FromStr for Message {
187
    type Err = anyhow::Error;
188

            
189
219
    fn from_str(message: &str) -> anyhow::Result<Self> {
190
219
        let args: Vec<&str> = message.split_whitespace().collect();
191

            
192
219
        let Some(first) = args.first() else {
193
            return Ok(Self::Empty);
194
        };
195

            
196
219
        match *first {
197
219
            "board_size" => Ok(Self::BoardSize(
198
                (*args
199
                    .get(1)
200
                    .context("expected: board_size 11 or board_size 13")?)
201
                .parse()?,
202
            )),
203
219
            "final_status" => Ok(Self::FinalStatus),
204
219
            "generate_move" => Ok(Self::GenerateMove),
205
219
            "known_command" => Ok(Self::KnownCommand(
206
                (*args.get(1).context("expected: known_command COMMAND")?).to_string(),
207
            )),
208
219
            "list_commands" => Ok(Self::ListCommands),
209
219
            "name" => Ok(Self::Name),
210
219
            "play" => {
211
219
                let play = Plae::try_from(args)?;
212
204
                Ok(Self::Play(play))
213
            }
214
            "play_from" => Ok(Self::PlayFrom),
215
            "play_to" => {
216
                if let (Some(role), Some(vertex)) = (args.get(1), args.get(2)) {
217
                    let role = Role::from_str(role)?;
218
                    let vertex = Vertex::from_str(vertex)?;
219
                    Ok(Self::PlayTo((role, vertex)))
220
                } else {
221
                    Err(anyhow::Error::msg("expected: play_to role vertex"))
222
                }
223
            }
224
            "protocol_version" => Ok(Self::ProtocolVersion),
225
            "quit" => Ok(Self::Quit),
226
            "show_board" => Ok(Self::ShowBoard),
227
            "time_settings" => {
228
                let time_settings = time::TimeSettings::try_from(args)?;
229
                Ok(Self::TimeSettings(time_settings))
230
            }
231
            "version" => Ok(Self::Version),
232
            text => {
233
                if text.trim().is_empty() {
234
                    Ok(Self::Empty)
235
                } else {
236
                    Err(anyhow::Error::msg(format!("unrecognized command: {text}")))
237
                }
238
            }
239
        }
240
219
    }
241
}