asciiplanes-player.sf 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. #!/usr/bin/ruby
  2. #`(if running under some shell) {
  3. eval 'exec /usr/bin/sidef $0 ${1+"$@"}'
  4. }
  5. # Author: Trizen
  6. # Date: 26 April 2023
  7. # https://github.com/trizen
  8. # Solver for the asciiplanes game.
  9. var asciitable =
  10. try { require('Text::ASCIITable') }
  11. catch { STDERR.print("Can't load the 'Text::ASCIITable' Perl module...\n"); Sys.exit(2) }
  12. var ANSI =
  13. try { frequire('Term::ANSIColor') }
  14. catch { nil }
  15. ## Package variables
  16. var pkgname = 'asciiplanes-player'
  17. var version = 0.01
  18. ## Game variables
  19. var BOARD_SIZE = 8
  20. var PLANES_NUM = 3
  21. define(
  22. AIR = '`',
  23. BLANK = ' ',
  24. HIT = 'O',
  25. HEAD = 'X',
  26. )
  27. var score_table = Hash(
  28. air => AIR,
  29. head => HEAD,
  30. hit => HIT,
  31. )
  32. var use_colors = defined(ANSI)
  33. var wrap_plane = false
  34. var simulate = false
  35. var hit_char = HIT
  36. var miss_char = AIR
  37. var head_char = HEAD
  38. var seed = 0
  39. func print_usage {
  40. print <<"EOT"
  41. usage: #{__MAIN__} [options]
  42. main:
  43. --size=i : length side of the board (default: #{BOARD_SIZE})
  44. --planes=i : the total number of planes (default: #{PLANES_NUM})
  45. --wrap! : wrap the plane around the play board (default: #{wrap_plane})
  46. --head=s : character used for the head of the plane (default: "#{head_char}")
  47. --hit=s : character used when a plane is hit (default: "#{hit_char}")
  48. --miss=s : character used when a plane is missed (default: "#{miss_char}")
  49. --colors! : use ANSI colors (requires Term::ANSIColor) (default: #{use_colors})
  50. --simulate! : run a random simulation (default: #{simulate})
  51. --seed=i : run with a given pseudorandom seed value > 0 (default: #{seed})
  52. help:
  53. --help : print this message and exit
  54. --version : print the version number and exit
  55. example:
  56. #{__MAIN__} --size=12 --planes=6 --hit='*'
  57. EOT
  58. Sys.exit
  59. }
  60. func print_version {
  61. print "#{pkgname} #{version}\n"
  62. Sys.exit
  63. }
  64. if (ARGV) {
  65. ARGV.getopt!(
  66. 'board-size|size=i' => \BOARD_SIZE,
  67. 'planes-num=i' => \PLANES_NUM,
  68. 'seed=i' => \seed,
  69. 'head-char=s' => \head_char,
  70. 'hit-char=s' => \hit_char,
  71. 'miss-char=s' => \miss_char,
  72. 'wrap!' => \wrap_plane,
  73. 'colors!' => \use_colors
  74. 'simulate!' => \simulate,
  75. 'help|h|?' => print_usage,
  76. 'version|v|V' => print_version,
  77. )
  78. }
  79. if (seed) {
  80. iseed(seed)
  81. Perl.eval("srand(#{seed})")
  82. }
  83. #---------------------------------------------------------------
  84. func pointers(board, x, y, indices) {
  85. gather {
  86. indices.each_2d { |i,j|
  87. var (row, col) = (x+i, y+j)
  88. if (wrap_plane) {
  89. row %= BOARD_SIZE
  90. col %= BOARD_SIZE
  91. }
  92. row.is_between(0, BOARD_SIZE-1) || return []
  93. col.is_between(0, BOARD_SIZE-1) || return []
  94. take(\board[row][col])
  95. }
  96. }
  97. }
  98. var up =
  99. [
  100. [+0, +0],
  101. [+1, -1], [+1, +0], [+1, +1],
  102. [+2, +0],
  103. [+3, -1], [+3, +0], [+3, +1],
  104. ]
  105. var down =
  106. [
  107. [-3, -1], [-3, +0], [-3, +1],
  108. [-2, +0],
  109. [-1, -1], [-1, +0], [-1, +1],
  110. [+0, +0],
  111. ]
  112. var left =
  113. [
  114. [-1, +1], [-1, +3],
  115. [+0, +0], [+0, +1], [+0, +2], [+0, +3],
  116. [+1, +1], [+1, +3],
  117. ]
  118. var right =
  119. [
  120. [-1, -3], [-1, -1],
  121. [+0, -3], [+0, -2], [+0, -1], [+0, +0],
  122. [+1, -3], [+1, -1],
  123. ]
  124. const DIRECTIONS = [up, down, left, right]
  125. const PAIR_INDICES =
  126. BOARD_SIZE.range.map {|i|
  127. BOARD_SIZE.range.map {|j|
  128. [i, j]
  129. }...
  130. }
  131. func assign(board, dir, x, y, force = false) {
  132. var plane = pointers(board, x, y, dir) || return false
  133. if (!force) {
  134. plane.all {|c| *c == BLANK } || return false
  135. }
  136. plane.each {|c| *c = HIT }
  137. board[x][y] = HEAD
  138. return true
  139. }
  140. func print_ascii_table(*boards) {
  141. var ascii_tables = []
  142. for board in (boards) {
  143. var table = asciitable.new(Hash(headingText => "#{pkgname} #{version}"))
  144. table.setCols(' ', (1..BOARD_SIZE)...)
  145. var char = 'a';
  146. board.each { |row|
  147. table.addRow([char++, row...])
  148. table.addRowLine()
  149. }
  150. var t = table.drawit
  151. if (defined(ANSI) && use_colors) {
  152. t.gsub!(HIT, ANSI.colored(hit_char, 'bold red'))
  153. t.gsub!(AIR, ANSI.colored(miss_char, 'yellow'))
  154. t.gsub!(HEAD, ANSI.colored(head_char, 'bold green'))
  155. }
  156. ascii_tables << t
  157. }
  158. ascii_tables.map { .lines }.zip {|*a|
  159. say a.join(' ')
  160. }
  161. }
  162. func valid_assignment (play_board, info_board, extra = false) {
  163. [play_board, info_board].zip {|*rows|
  164. rows.zip {|play,info|
  165. if (info == AIR) {
  166. if (play != BLANK) {
  167. return false
  168. }
  169. }
  170. elsif (extra) {
  171. info == BLANK && next
  172. if (info != play) {
  173. return false
  174. }
  175. }
  176. }
  177. }
  178. return true
  179. }
  180. func create_planes(play_board) {
  181. var count = 0
  182. var max_tries = BOARD_SIZE**4
  183. while (count != PLANES_NUM) {
  184. var x = irand(1, BOARD_SIZE)-1
  185. var y = irand(1, BOARD_SIZE)-1
  186. var dir = DIRECTIONS.rand
  187. if (--max_tries <= 0) {
  188. die "FATAL ERROR: try to increase the size of the grid (--size=x).\n"
  189. }
  190. assign(play_board, dir, x, y) || next
  191. ++count
  192. }
  193. return true
  194. }
  195. func guess(info_board, play_board, plane_count) {
  196. var count = 0
  197. var max_tries = BOARD_SIZE*BOARD_SIZE
  198. var indices = PAIR_INDICES.shuffle
  199. while (count != (PLANES_NUM - plane_count)) {
  200. #var x = irand(1, BOARD_SIZE)-1
  201. #var y = irand(1, BOARD_SIZE)-1
  202. var (x,y) = (indices.pop_rand \\ return nil)...
  203. loop {
  204. (play_board[x][y] == BLANK) && (info_board[x][y] == BLANK) && break
  205. (x,y) = (indices.pop_rand \\ return nil)...
  206. }
  207. if (--max_tries <= 0) {
  208. return nil
  209. }
  210. var good_directions = DIRECTIONS.grep {|dir|
  211. var plane = pointers(info_board, x, y, dir)
  212. plane && plane.none { *_ == AIR }
  213. } || next
  214. good_directions.shuffle.any {|dir|
  215. assign(play_board, dir, x, y)
  216. } || next
  217. #valid_assignment(play_board, info_board) || return nil
  218. ++count
  219. }
  220. return true
  221. }
  222. func get_head_positions(board) {
  223. var headshots = []
  224. board.each_kv {|i,row|
  225. row.each_kv {|j,entry|
  226. if (entry == HEAD) {
  227. headshots << [i,j]
  228. }
  229. }
  230. }
  231. return headshots
  232. }
  233. func make_play_board {
  234. BOARD_SIZE.of { BOARD_SIZE.of { BLANK } }
  235. }
  236. func make_play_boards(info_board) {
  237. var headshots = get_head_positions(info_board)
  238. var boards = [
  239. [make_play_board(), 0]
  240. ]
  241. for x,y in (headshots), dir in (DIRECTIONS) {
  242. for board,count in (boards.map { .dclone }) {
  243. assign(board, dir, x, y) || next
  244. boards << [board, count+1]
  245. }
  246. }
  247. var max_count = boards.map { .tail }.max
  248. boards.grep { .tail == max_count }.grep { valid_assignment(.head, info_board) }
  249. }
  250. func get_letters() {
  251. var letters = Hash()
  252. var char = 'a'
  253. BOARD_SIZE.range.each { |i|
  254. letters{char++} = i
  255. }
  256. return letters
  257. }
  258. func solve(callback) {
  259. var tries = 0
  260. var info_board = make_play_board()
  261. var boards = make_play_boards(info_board)
  262. loop {
  263. for board,plane_count in (boards) {
  264. var play_board = board.dclone
  265. guess(info_board, play_board, plane_count) || next
  266. valid_assignment(play_board, info_board, true) || next
  267. var all_dead = true
  268. var new_info = false
  269. # Prefer points nearest to the center of the board
  270. var head_pos = get_head_positions(play_board).sort_by {|p|
  271. hypot(p.map {|i| (BOARD_SIZE-1)/2 - i }...)
  272. }
  273. head_pos = head_pos.grep_2d {|x,y| info_board[x][y] == BLANK }.map_2d {|x,y|
  274. [x, y, DIRECTIONS.map {|d| pointers(info_board, x, y, d) }.grep {|t|
  275. t && t.none { *_ == AIR }
  276. }]
  277. }
  278. # Prefer the planes with the most hits
  279. head_pos = head_pos.sort_by {|p|
  280. p[2].sum_by {|t| t.count_by { *_ == HIT } } -> neg
  281. }
  282. head_pos.each_2d {|i,j|
  283. if (info_board[i][j] != BLANK) {
  284. next
  285. }
  286. all_dead = false
  287. var score = callback(i, j, play_board, info_board) \\ return nil
  288. if (score == BLANK) {
  289. score = AIR
  290. }
  291. ++tries
  292. info_board[i][j] = score
  293. if (score == HEAD) {
  294. new_info = true
  295. boards = make_play_boards(info_board)
  296. next
  297. }
  298. elsif (score == AIR) {
  299. new_info = true
  300. boards = boards.grep { valid_assignment(.head, info_board) }.flip
  301. }
  302. break
  303. }
  304. if (all_dead) {
  305. return tries
  306. }
  307. break if new_info
  308. }
  309. }
  310. }
  311. var letters2indices = get_letters()
  312. var indices2letters = letters2indices.flip
  313. func process_user_input(i, j, play_board, info_board) {
  314. print_ascii_table(play_board, info_board)
  315. loop {
  316. say "=> My guess: #{indices2letters{i}}#{j+1}"
  317. say "=> Score (hit, head or air)"
  318. var input = (Sys.scanln("> ") \\ return nil -> lc)
  319. input ~~ ['q', 'quit'] && return nil
  320. input.trim!
  321. score_table.has(input) || do {
  322. say "\n:: Invalid score...\n"
  323. next
  324. }
  325. return score_table{input}
  326. }
  327. }
  328. if (simulate) {
  329. var board = make_play_board()
  330. create_planes(board)
  331. var tries = solve(func(i, j, play_board, info_board) {
  332. print_ascii_table(play_board, info_board)
  333. board[i][j]
  334. })
  335. say "It took #{tries} tries to solve:"
  336. print_ascii_table(board)
  337. }
  338. else {
  339. var tries = solve(process_user_input)
  340. if (defined(tries)) {
  341. say "\n:: All planes destroyed in #{tries} tries!\n"
  342. }
  343. }