123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455 |
- #!/usr/bin/ruby
- #`(if running under some shell) {
- eval 'exec /usr/bin/sidef $0 ${1+"$@"}'
- }
- # Author: Trizen
- # Date: 26 April 2023
- # https://github.com/trizen
- # Solver for the asciiplanes game.
- var asciitable =
- try { require('Text::ASCIITable') }
- catch { STDERR.print("Can't load the 'Text::ASCIITable' Perl module...\n"); Sys.exit(2) }
- var ANSI =
- try { frequire('Term::ANSIColor') }
- catch { nil }
- ## Package variables
- var pkgname = 'asciiplanes-player'
- var version = 0.01
- ## Game variables
- var BOARD_SIZE = 8
- var PLANES_NUM = 3
- define(
- AIR = '`',
- BLANK = ' ',
- HIT = 'O',
- HEAD = 'X',
- )
- var score_table = Hash(
- air => AIR,
- head => HEAD,
- hit => HIT,
- )
- var use_colors = defined(ANSI)
- var wrap_plane = false
- var simulate = false
- var hit_char = HIT
- var miss_char = AIR
- var head_char = HEAD
- var seed = 0
- func print_usage {
- print <<"EOT"
- usage: #{__MAIN__} [options]
- main:
- --size=i : length side of the board (default: #{BOARD_SIZE})
- --planes=i : the total number of planes (default: #{PLANES_NUM})
- --wrap! : wrap the plane around the play board (default: #{wrap_plane})
- --head=s : character used for the head of the plane (default: "#{head_char}")
- --hit=s : character used when a plane is hit (default: "#{hit_char}")
- --miss=s : character used when a plane is missed (default: "#{miss_char}")
- --colors! : use ANSI colors (requires Term::ANSIColor) (default: #{use_colors})
- --simulate! : run a random simulation (default: #{simulate})
- --seed=i : run with a given pseudorandom seed value > 0 (default: #{seed})
- help:
- --help : print this message and exit
- --version : print the version number and exit
- example:
- #{__MAIN__} --size=12 --planes=6 --hit='*'
- EOT
- Sys.exit
- }
- func print_version {
- print "#{pkgname} #{version}\n"
- Sys.exit
- }
- if (ARGV) {
- ARGV.getopt!(
- 'board-size|size=i' => \BOARD_SIZE,
- 'planes-num=i' => \PLANES_NUM,
- 'seed=i' => \seed,
- 'head-char=s' => \head_char,
- 'hit-char=s' => \hit_char,
- 'miss-char=s' => \miss_char,
- 'wrap!' => \wrap_plane,
- 'colors!' => \use_colors
- 'simulate!' => \simulate,
- 'help|h|?' => print_usage,
- 'version|v|V' => print_version,
- )
- }
- if (seed) {
- iseed(seed)
- %Perl{srand(#{seed})}
- }
- #---------------------------------------------------------------
- func pointers(board, x, y, indices) {
- gather {
- indices.each_2d { |i,j|
- var (row, col) = (x+i, y+j)
- if (wrap_plane) {
- row %= BOARD_SIZE
- col %= BOARD_SIZE
- }
- row.is_between(0, BOARD_SIZE-1) || return []
- col.is_between(0, BOARD_SIZE-1) || return []
- take(\board[row][col])
- }
- }
- }
- var up =
- [
- [+0, +0],
- [+1, -1], [+1, +0], [+1, +1],
- [+2, +0],
- [+3, -1], [+3, +0], [+3, +1],
- ]
- var down =
- [
- [-3, -1], [-3, +0], [-3, +1],
- [-2, +0],
- [-1, -1], [-1, +0], [-1, +1],
- [+0, +0],
- ]
- var left =
- [
- [-1, +1], [-1, +3],
- [+0, +0], [+0, +1], [+0, +2], [+0, +3],
- [+1, +1], [+1, +3],
- ]
- var right =
- [
- [-1, -3], [-1, -1],
- [+0, -3], [+0, -2], [+0, -1], [+0, +0],
- [+1, -3], [+1, -1],
- ]
- const DIRECTIONS = [up, down, left, right]
- const PAIR_INDICES =
- BOARD_SIZE.range.map {|i|
- BOARD_SIZE.range.map {|j|
- [i, j]
- }...
- }
- func assign(board, dir, x, y, force = false) {
- var plane = pointers(board, x, y, dir) || return false
- if (!force) {
- plane.all {|c| *c == BLANK } || return false
- }
- plane.each {|c| *c = HIT }
- board[x][y] = HEAD
- return true
- }
- func print_ascii_table(*boards) {
- var ascii_tables = []
- for board in (boards) {
- var table = asciitable.new(Hash(headingText => "#{pkgname} #{version}"))
- table.setCols(' ', (1..BOARD_SIZE)...)
- var char = 'a';
- board.each { |row|
- table.addRow([char++, row...])
- table.addRowLine()
- }
- var t = table.drawit
- if (defined(ANSI) && use_colors) {
- t.gsub!(HIT, ANSI.colored(hit_char, 'bold red'))
- t.gsub!(AIR, ANSI.colored(miss_char, 'yellow'))
- t.gsub!(HEAD, ANSI.colored(head_char, 'bold green'))
- }
- ascii_tables << t
- }
- ascii_tables.map { .lines }.zip {|*a|
- say a.join(' ')
- }
- }
- func valid_assignment (play_board, info_board, extra = false) {
- [play_board, info_board].zip {|*rows|
- rows.zip {|play,info|
- if (info == AIR) {
- if (play != BLANK) {
- return false
- }
- }
- elsif (extra) {
- info == BLANK && next
- if (info != play) {
- return false
- }
- }
- }
- }
- return true
- }
- func create_planes(play_board) {
- var count = 0
- var max_tries = BOARD_SIZE**4
- while (count != PLANES_NUM) {
- var x = irand(1, BOARD_SIZE)-1
- var y = irand(1, BOARD_SIZE)-1
- var dir = DIRECTIONS.rand
- if (--max_tries <= 0) {
- die "FATAL ERROR: try to increase the size of the grid (--size=x).\n"
- }
- assign(play_board, dir, x, y) || next
- ++count
- }
- return true
- }
- func guess(info_board, play_board, plane_count) {
- var count = 0
- var max_tries = BOARD_SIZE*BOARD_SIZE
- var indices = PAIR_INDICES.shuffle
- while (count != (PLANES_NUM - plane_count)) {
- #var x = irand(1, BOARD_SIZE)-1
- #var y = irand(1, BOARD_SIZE)-1
- var (x,y) = (indices.pop_rand \\ return nil)...
- loop {
- (play_board[x][y] == BLANK) && (info_board[x][y] == BLANK) && break
- (x,y) = (indices.pop_rand \\ return nil)...
- }
- if (--max_tries <= 0) {
- return nil
- }
- var good_directions = DIRECTIONS.grep {|dir|
- var plane = pointers(info_board, x, y, dir)
- plane && plane.none { *_ == AIR }
- } || next
- good_directions.shuffle.any {|dir|
- assign(play_board, dir, x, y)
- } || next
- #valid_assignment(play_board, info_board) || return nil
- ++count
- }
- return true
- }
- func get_head_positions(board) {
- var headshots = []
- board.each_kv {|i,row|
- row.each_kv {|j,entry|
- if (entry == HEAD) {
- headshots << [i,j]
- }
- }
- }
- return headshots
- }
- func make_play_board {
- BOARD_SIZE.of { BOARD_SIZE.of { BLANK } }
- }
- func make_play_boards(info_board) {
- var headshots = get_head_positions(info_board)
- var boards = [
- [make_play_board(), 0]
- ]
- for x,y in (headshots), dir in (DIRECTIONS) {
- for board,count in (boards.map { .dclone }) {
- assign(board, dir, x, y) || next
- boards << [board, count+1]
- }
- }
- var max_count = boards.map { .tail }.max
- boards.grep { .tail == max_count }.grep { valid_assignment(.head, info_board) }
- }
- func get_letters() {
- var letters = Hash()
- var char = 'a'
- BOARD_SIZE.range.each { |i|
- letters{char++} = i
- }
- return letters
- }
- func solve(callback) {
- var tries = 0
- var info_board = make_play_board()
- var boards = make_play_boards(info_board)
- loop {
- for board,plane_count in (boards) {
- var play_board = board.dclone
- guess(info_board, play_board, plane_count) || next
- valid_assignment(play_board, info_board, true) || next
- var all_dead = true
- var new_info = false
- # Prefer points nearest to the center of the board
- var head_pos = get_head_positions(play_board).sort_by {|p|
- hypot(p.map {|i| (BOARD_SIZE-1)/2 - i }...)
- }
- head_pos = head_pos.grep_2d {|x,y| info_board[x][y] == BLANK }.map_2d {|x,y|
- [x, y, DIRECTIONS.map {|d| pointers(info_board, x, y, d) }.grep {|t|
- t && t.none { *_ == AIR }
- }]
- }
- # Prefer the planes with the most hits
- head_pos = head_pos.sort_by {|p|
- p[2].sum_by {|t| t.count_by { *_ == HIT } } -> neg
- }
- head_pos.each_2d {|i,j|
- if (info_board[i][j] != BLANK) {
- next
- }
- all_dead = false
- var score = callback(i, j, play_board, info_board) \\ return nil
- if (score == BLANK) {
- score = AIR
- }
- ++tries
- info_board[i][j] = score
- if (score == HEAD) {
- new_info = true
- boards = make_play_boards(info_board)
- next
- }
- elsif (score == AIR) {
- new_info = true
- boards = boards.grep { valid_assignment(.head, info_board) }.flip
- }
- break
- }
- if (all_dead) {
- return tries
- }
- break if new_info
- }
- }
- }
- var letters2indices = get_letters()
- var indices2letters = letters2indices.flip
- func process_user_input(i, j, play_board, info_board) {
- print_ascii_table(play_board, info_board)
- loop {
- say "=> My guess: #{indices2letters{i}}#{j+1}"
- say "=> Score (hit, head or air)"
- var input = (Sys.scanln("> ") \\ return nil -> lc)
- input ~~ ['q', 'quit'] && return nil
- input.trim!
- score_table.has(input) || do {
- say "\n:: Invalid score...\n"
- next
- }
- return score_table{input}
- }
- }
- if (simulate) {
- var board = make_play_board()
- create_planes(board)
- var tries = solve(func(i, j, play_board, info_board) {
- print_ascii_table(play_board, info_board)
- board[i][j]
- })
- say "It took #{tries} tries to solve:"
- print_ascii_table(board)
- }
- else {
- var tries = solve(process_user_input)
- if (defined(tries)) {
- say "\n:: All planes destroyed in #{tries} tries!\n"
- }
- }
|