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
use std::str::FromStr;
17

            
18
use anyhow::Context;
19

            
20
use crate::{
21
    play::{Plae, Vertex},
22
    role::Role,
23
    time,
24
};
25

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

            
95
    /// The empty string or only comments and whitespace was passed.
96
    Empty,
97

            
98
    /// `final_status`
99
    ///
100
    /// Returns `attacker_wins` or `draw` or `ongoing` or `defender_wins`.
101
    FinalStatus,
102

            
103
    /// `generate_move`
104
    ///
105
    /// Returns `play ROLE FROM TO`.
106
    GenerateMove,
107

            
108
    /// `known_command STRING`
109
    ///
110
    /// Returns a boolean signifying whether the engine knows the command.
111
    KnownCommand(String),
112

            
113
    /// `list_commands`
114
    ///
115
    /// Lists all of the known commands, each separated by a newline.
116
    ListCommands,
117

            
118
    /// `name`
119
    ///
120
    /// Prints the name of the package.
121
    Name,
122

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

            
128
    /// `play_from`
129
    ///
130
    /// Returns a **ROLE** followed by all the valid **FROM** squares.
131
    PlayFrom,
132

            
133
    /// `play_to ROLE FROM`
134
    ///
135
    /// Returns all the valid **TO** squares.
136
    PlayTo((Role, Vertex)),
137

            
138
    /// `protocol_version`
139
    ///
140
    /// Prints the version of the Hnefatafl Text Protocol.
141
    ProtocolVersion,
142

            
143
    /// `quit`
144
    ///
145
    /// quits the engine.
146
    Quit,
147

            
148
    /// `show_board`
149
    ///
150
    /// Displays the board
151
    ShowBoard,
152

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

            
160
    /// `version`
161
    ///
162
    /// Displays the package version.
163
    Version,
164
}
165

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

            
183
impl FromStr for Message {
184
    type Err = anyhow::Error;
185

            
186
438
    fn from_str(message: &str) -> anyhow::Result<Self> {
187
438
        let args: Vec<&str> = message.split_whitespace().collect();
188

            
189
438
        let Some(first) = args.first() else {
190
            return Ok(Self::Empty);
191
        };
192

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