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
#![deny(clippy::expect_used)]
20
#![deny(clippy::indexing_slicing)]
21
#![deny(clippy::panic)]
22
#![deny(clippy::unwrap_used)]
23

            
24
use std::{
25
    io::{self, BufReader, Write},
26
    net::TcpStream,
27
};
28

            
29
use clap::{self, CommandFactory, Parser};
30

            
31
use hnefatafl_copenhagen::{
32
    COPYRIGHT, SERVER_PORT,
33
    ai::AI,
34
    game::Game,
35
    play::Plae,
36
    read_response,
37
    status::Status,
38
    utils::{choose_ai, clear_screen},
39
    write_command,
40
};
41

            
42
/// Hnefatafl Copenhagen
43
///
44
/// This plays the game using the Hnefatafl Text Protocol.
45
#[derive(Parser, Debug)]
46
#[command(version, about = "Copenhagen Hnefatafl Engine")]
47
struct Args {
48
    /// Choose an AI to play as
49
    #[arg(long)]
50
    ai: Option<String>,
51

            
52
    /// Displays the game
53
    #[arg(long)]
54
    display_game: bool,
55

            
56
    /// How many seconds to run the monte-carlo AI
57
    #[arg(long)]
58
    seconds: Option<u64>,
59

            
60
    /// How deep in the game tree to go with Ai
61
    #[arg(long)]
62
    depth: Option<u8>,
63

            
64
    /// Listen for HTP drivers on host
65
    #[arg(long)]
66
    host: Option<String>,
67

            
68
    /// Render everything in ASCII
69
    #[arg(long)]
70
    ascii: bool,
71

            
72
    /// Build the manpage
73
    #[arg(long)]
74
    man: bool,
75
}
76

            
77
fn main() -> anyhow::Result<()> {
78
    let args = Args::parse();
79

            
80
    if args.man {
81
        let mut buffer: Vec<u8> = Vec::default();
82
        let cmd = Args::command()
83
            .name("hnefatafl-text-protocol")
84
            .long_version(None);
85
        let man = clap_mangen::Man::new(cmd).date("2025-11-17");
86

            
87
        man.render(&mut buffer)?;
88
        write!(buffer, "{COPYRIGHT}")?;
89

            
90
        std::fs::write("hnefatafl-text-protocol.1", buffer)?;
91
        return Ok(());
92
    }
93

            
94
    let args = Args::parse();
95

            
96
    let mut game = Game::default();
97
    if args.ascii {
98
        game.chars.ascii();
99
        game.board.display_ascii = true;
100
    }
101

            
102
    if let Some(mut address) = args.host {
103
        address.push_str(SERVER_PORT);
104

            
105
        let ai = match args.ai {
106
            Some(ai) => choose_ai(&ai, args.seconds, args.depth, true)?,
107
            None => choose_ai("basic", args.seconds, args.depth, true)?,
108
        };
109

            
110
        play_tcp(game, ai, &address, args.display_game)?;
111
    } else if let Some(ai) = args.ai {
112
        let ai = choose_ai(&ai, args.seconds, args.depth, true)?;
113

            
114
        play_ai(game, ai, args.display_game)?;
115
    } else {
116
        play(game, args.display_game)?;
117
    }
118

            
119
    Ok(())
120
}
121

            
122
fn play(mut game: Game, display_game: bool) -> anyhow::Result<()> {
123
    let mut buffer = String::new();
124
    let stdin = io::stdin();
125

            
126
    if display_game {
127
        clear_screen()?;
128
        println!("{game}\n");
129
        println!("Enter 'list_commands' for a list of commands.");
130
    }
131

            
132
    loop {
133
        if let Err(error) = stdin.read_line(&mut buffer) {
134
            println!("? {error}\n");
135
            buffer.clear();
136
            return Ok(());
137
        }
138

            
139
        let result = game.read_line(&buffer);
140

            
141
        if display_game {
142
            clear_screen()?;
143
            println!("{game}\n");
144
        }
145

            
146
        match result {
147
            Err(error) => println!("? {error}\n"),
148
            Ok(message) => {
149
                if let Some(message) = message {
150
                    println!("= {message}");
151
                }
152
            }
153
        }
154

            
155
        buffer.clear();
156
    }
157
}
158

            
159
fn play_ai(mut game: Game, mut ai: Box<dyn AI>, display_game: bool) -> anyhow::Result<()> {
160
    let mut buffer = String::new();
161

            
162
    if display_game {
163
        clear_screen()?;
164
        println!("{game}\n");
165
    }
166

            
167
    loop {
168
        let generate_move = ai.generate_move(&mut game)?;
169

            
170
        if display_game {
171
            clear_screen()?;
172
            println!("{game}\n");
173
        }
174

            
175
        println!("= {generate_move}");
176

            
177
        if game.status != Status::Ongoing {
178
            return Ok(());
179
        }
180

            
181
        buffer.clear();
182
    }
183
}
184

            
185
fn play_tcp(
186
    mut game: Game,
187
    mut ai: Box<dyn AI>,
188
    address: &str,
189
    display_game: bool,
190
) -> anyhow::Result<()> {
191
    let mut stream = TcpStream::connect(address)?;
192
    println!("connected to {address} ...");
193

            
194
    let mut reader = BufReader::new(stream.try_clone()?);
195
    for i in 1..10_000 {
196
        println!("\n*** turn {i} ***");
197

            
198
        let message = read_response(&mut reader)?;
199

            
200
        let words: Vec<_> = message.as_str().split_ascii_whitespace().collect();
201

            
202
        if let Some(word) = words.first() {
203
            match *word {
204
                "play" => {
205
                    let play = Plae::try_from(words)?;
206
                    ai.play(&mut game, &play)?;
207

            
208
                    if display_game {
209
                        println!("{game}");
210
                    }
211
                }
212
                "generate_move" => {
213
                    let generate_move = ai.generate_move(&mut game)?;
214
                    write_command(&format!("{}\n", generate_move.play), &mut stream)?;
215

            
216
                    if display_game {
217
                        println!("{game}");
218
                    }
219

            
220
                    println!("{generate_move}");
221
                    println!("{}", generate_move.heat_map);
222
                }
223
                _ => match game.read_line(&message) {
224
                    Err(error) => println!("? {error}\n"),
225
                    Ok(message) => {
226
                        if let Some(message) = message {
227
                            println!("= {message}");
228
                        }
229
                    }
230
                },
231
            }
232
        }
233

            
234
        if game.status != Status::Ongoing {
235
            break;
236
        }
237
    }
238

            
239
    Ok(())
240
}