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
#![deny(clippy::expect_used)]
17
#![deny(clippy::indexing_slicing)]
18
#![deny(clippy::panic)]
19
#![deny(clippy::unwrap_used)]
20

            
21
use std::{
22
    io::{self, BufReader, Write},
23
    net::TcpStream,
24
};
25

            
26
use clap::{self, CommandFactory, Parser};
27

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

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

            
49
    /// Displays the game
50
    #[arg(long)]
51
    display_game: bool,
52

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

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

            
61
    /// Listen for HTP drivers on host
62
    #[arg(long)]
63
    host: Option<String>,
64

            
65
    /// Render everything in ASCII
66
    #[arg(long)]
67
    ascii: bool,
68

            
69
    /// Build the manpage
70
    #[arg(long)]
71
    man: bool,
72
}
73

            
74
fn main() -> anyhow::Result<()> {
75
    let args = Args::parse();
76

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

            
84
        man.render(&mut buffer)?;
85
        write!(buffer, "{COPYRIGHT}")?;
86

            
87
        std::fs::write("hnefatafl-text-protocol.1", buffer)?;
88
        return Ok(());
89
    }
90

            
91
    let args = Args::parse();
92

            
93
    let mut game = Game::default();
94
    if args.ascii {
95
        game.chars.ascii();
96
        game.board.display_ascii = true;
97
    }
98

            
99
    if let Some(mut address) = args.host {
100
        address.push_str(SERVER_PORT);
101

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

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

            
111
        play_ai(game, ai, args.display_game)?;
112
    } else {
113
        play(game, args.display_game)?;
114
    }
115

            
116
    Ok(())
117
}
118

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

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

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

            
136
        let result = game.read_line(&buffer);
137

            
138
        if display_game {
139
            clear_screen()?;
140
            println!("{game}\n");
141
        }
142

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

            
152
        buffer.clear();
153
    }
154
}
155

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

            
159
    if display_game {
160
        clear_screen()?;
161
        println!("{game}\n");
162
    }
163

            
164
    loop {
165
        let generate_move = ai.generate_move(&mut game)?;
166

            
167
        if display_game {
168
            clear_screen()?;
169
            println!("{game}\n");
170
        }
171

            
172
        println!("= {generate_move}");
173

            
174
        if game.status != Status::Ongoing {
175
            return Ok(());
176
        }
177

            
178
        buffer.clear();
179
    }
180
}
181

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

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

            
195
        let message = read_response(&mut reader)?;
196

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

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

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

            
213
                    if display_game {
214
                        println!("{game}");
215
                    }
216

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

            
231
        if game.status != Status::Ongoing {
232
            break;
233
        }
234
    }
235

            
236
    Ok(())
237
}