View difference between Paste ID: eB6upixP and YHSUmz42
SHOW: | | - or go back to the newest paste.
1
-- Stratego version B2.1 by KnightMiner
2
-- check if we are hosting or joining a game
3
local args = { ... }
4
if #args > 3 or #args < 2 or ( args[1] ~= 'host' and args[1] ~= 'join' ) then
5
  print( 'Usages:' )
6
  print( 'stratego host [save-game] <name>' )
7
  print( 'stratego join <name>' )
8
  return
9
end
10
11
-- find a modem to host/join a game on
12
local openedModem, foundModem
13
for _, modem in ipairs( peripheral.getNames() ) do
14
  if peripheral.getType( modem ) == "modem" then
15
    if not rednet.isOpen( modem ) then
16
      rednet.open( modem )
17
      openedModem = modem
18
    end
19
    foundModem = true
20
    break
21
  end
22
end
23
if not foundModem then
24
  print( "No modems found" )
25
  return
26
end
27
28
-- function for shutdown, including closing the modem and clearing the screen
29
-- it also prints an optional message before the "Thanks" message
30
function shutdown( msg )
31
  shell.run( 'clear' )
32
  if openedModem then
33
    rednet.close( openedModem )
34
  end
35
  if msg then
36
    print( msg )
37
  end
38
  print( 'Thank you for playing Stratego by KnightMiner' )
39
end
40
41
-- load the config from the file "stratego.cfg"
42
-- note that the file needs to be in the current directory to load,
43
-- getting it to load from the same directory as the game proves quite hard
44
local config, configMsg
45
local cfgFile = shell.resolve( 'stratego.cfg' )
46
if fs.exists( cfgFile ) then
47
  local file = fs.open( cfgFile, 'r' )
48
  config = textutils.unserialize( file.readAll() )
49
  file.close()
50
  if not config then
51
    -- we write the message as a variable as the writeLog function requires the config to load,
52
    -- and any print will be overwritten in the next step
53
    configMsg = 'Failed to load config'
54
  else
55
    configMsg = 'Successfully loaded config'
56
  end
57
end
58
-- if the config table is invalid or we didn't load one, replace it with a blank table
59
config = config or {}
60
  
61
-- define display stuff based on computer type
62
shell.run( 'clear' )
63
local display
64
-- pocket computers, which define an empty "pocket" table
65
if pocket then
66
  display = {
67
    write = {
68
      '+----------+-------------+',
69
      '|          |             |',
70
      '|          |   Stratego  |',
71
      '|          |             |',
72
      '|          |             |',
73
      '|          +-------------+',
74
      '|          |             |',
75
      '|          |             |',
76
      '|          |             |',
77
      '+----------+-------------+',
78
      '|                        |',
79
      '|                        |',
80
      '|                        |',
81
      '|                        |',
82
      '|                        |',
83
      '|                        |',
84
      '|                        |',
85
      '|                        |',
86
      '|                        |',
87
      '|                        |'
88
    },
89
    input = { 13, 7, 13 },
90
    log   = { 2, 11, 24, 11 },
91
    board = { 2, 2 }
92
  }
93
-- turtle computers, with the "turtle" api
94
elseif turtle then
95
  display = {
96
    write = {
97
      '+----------+----------+---------------+',
98
      '|          |          |               |',
99
      '|          | Stratego |               |',
100
      '|          |          |               |',
101
      '|          +----------+---------------+',
102
      '|          |                          |',
103
      '|          |                          |',
104
      '|          |                          |',
105
      '|          |                          |',
106
      '+----------+                          |',
107
      '+----------+                          |',
108
      '+----------+                          |',
109
      '+----------+                          |'
110
    },
111
    input = { 24, 2, 15 },
112
    log   = { 13, 6, 26, 9 },
113
    board = { 2, 2 }
114
  }
115
-- standard computers
116
else
117
  display = {
118
    write = {
119
      '+--------------------+----------+-----------------+',
120
      '|                    |          |                 |',
121
      '|                    | Stratego |                 |',
122
      '|                    |          |                 |',
123
      '|                    +----------+-----------------+',
124
      '|                    |                            |',
125
      '|                    |                            |',
126
      '|                    |                            |',
127
      '|                    |                            |',
128
      '|                    |                            |',
129
      '|                    |                            |',
130
      '|                    |                            |',
131
      '|                    |                            |',
132
      '|                    |                            |',
133
      '|                    |                            |',
134
      '|                    |                            |',
135
      '|                    |                            |',
136
      '+--------------------+                            |',
137
      '+--------------------+                            |'
138
    },
139
    input = { 34, 2, 17 },
140
    log   = { 23, 6, 28, 15 },
141
    board = { 2, 2, large = true }
142
  }
143
end
144
-- if there is config display stuff to load, overwrite any values with ones from the config
145
if config.display then
146
  for k in pairs( config.display ) do
147
    display[k] = config.display[k]
148
  end
149
end
150
151
-- fixes for multishell
152
if multishell and multishell.getCount() > 1 then
153
  table.remove( display.write )
154
  display.log[4] = display.log[4] - 1
155
end
156
157
-- draw the game board's borders
158
write( table.concat( display.write, '\n' ) )
159
160
-- create the output log as a window
161
local logBox = window.create( term.current(), display.log[1], display.log[2], display.log[3], display.log[4] )
162
function writeLog( ... )
163
  local prev = term.redirect( logBox )
164
  print( ... )
165
  term.redirect( prev )
166
end
167
168
-- write the config message from eariler
169
-- helps with debug in case you are wondering why your config is not working
170
if configMsg then
171
  writeLog( configMsg )
172
end
173
-- create the input box as a window
174
local inputBox = window.create( term.current(), display.input[1], display.input[2], display.input[3], 3 )
175
176
-- function to clear the input box
177
function inputClear( n )
178
  inputBox.setCursorPos( 1, 1 + ( n or 0 ) )
179
  inputBox.clearLine()
180
end
181
182
-- function to get user input
183
function input( msg )
184
  inputBox.setCursorPos( 1, 2 )
185
  inputBox.clearLine()
186
  if msg then
187
    inputBox.write( msg )
188
  end
189
  return read()
190
end
191
192
-- function to write to the input box
193
function inputWrite( msg, n )
194
  inputClear( n )
195
  inputBox.setCursorPos( 1, 1 + ( n or 0 ) )
196
  inputBox.clearLine()
197
  inputBox.write( msg )
198
end
199
200
-- define the pieces table, pieces will be added later
201
local pieces = {
202
  [1] = {},
203
  [2] = {},
204
  [3] = {},
205
  [4] = {},
206
  [5] = {},
207
  [6] = {},
208
  [7] = {},
209
  [8] = {}
210
}
211
212
-- check if a click is valid
213
-- used within the main click function, which is defined based on whether a mouse exists
214
local myTeam, notMyTeam, options, rows
215
function validClick( x, y, mode, px, py )
216
  -- location is out of bounds
217
  if x > 10 or x < 1 or y > 8 or y < 1 then
218
    writeLog( 'Invalid location' )
219
    return false
220
  -- location is beyond the legal columns to set up pieces
221
  -- note that "rows" and "options" are both defined later, but the click function is needed before they are defined
222
  elseif mode == 'setup' and options.rotate and ( myTeam == 'red' and ( x > rows ) or myTeam == 'blue' and ( x < ( 11 - rows ))) then
223
    writeLog( 'Cannot place pieces beyond first ' .. rows .. ' columns' )
224
    return false
225
  -- location is beyond the legal rows to set up pieces
226
  elseif mode == 'setup' and not options.rotate and ( myTeam == 'red' and ( y < ( 9 - rows ) ) or myTeam == 'blue' and ( y > rows )) then
227
    writeLog( 'Cannot place pieces beyond first ' .. rows .. ' rows' )
228
    return false
229
  -- check here if we are deselecting the piece, as otherwise it fails later as being the same team
230
  elseif mode == 'attack' and x == px and y == py then
231
    return 'cancel'
232
  else
233
    -- check reasons we are not allowed to move
234
    local piece = pieces[y][x]
235
    -- if no piece exists, then
236
    if not piece then
237
      -- if we are setting up or attacking, that is fine
238
      if mode then
239
        return true
240
      else
241
        -- otherwise we are moving, where we must start at a piece
242
        writeLog( 'No piece here' )
243
        return false
244
      end
245
    -- if the piece is a lake
246
    elseif piece[1] == 'lake' then
247
      if mode == 'setup' then
248
        writeLog( 'Cannot place pieces on lakes' )
249
      elseif mode == 'attack' then
250
        writeLog( 'Cannot move onto lakes' )
251
      else
252
        writeLog( 'Cannot move lakes' )
253
      end
254
      return false
255
    -- if the mode is not "attack" and the piece is not my team
256
    elseif mode ~= 'attack' and piece[1] == notMyTeam then
257
      writeLog( 'Cannot move other team' )
258
      return false
259
    -- if 
260
    elseif mode == 'attack' and piece[1] == myTeam then
261
      writeLog( 'Cannot move on to own team' )
262
      return false
263
    -- if the piece cannot move, either flags or bombs
264
    elseif not mode and ( piece[2] == 'F' or piece[2] == '*' ) then
265
      writeLog( 'Immovable piece' )
266
      return false
267
    -- if all cases pass as false, return movable
268
    else
269
      return true
270
    end
271
  end
272
end
273
274
-- determine if we are using an advance system or a regular one
275
-- then define the colors and the click() function based on that
276
local names = {
277
  ['*'] = 'bomb',
278
  ['!'] = 10,
279
  S = 'spy',
280
  F = 'flag',
281
  [1] = 'spotter'
282
}
283
local color = {}
284
local size = term.getSize()
285
local click, button
286
local isColor = term.isColor()
287
if isColor then
288
  names.red  = 'red'
289
  names.blue = 'blue'
290
  color = {
291
    red        = colors.red,
292
    redSelect  = colors.orange,
293
    blue       = colors.blue,
294
    blueSelect = colors.purple,
295
    textSelect = colors.white,
296
    lake       = colors.lightBlue,
297
    lakeBg     = colors.blue,
298
    pos        = colors.lime,
299
    neg        = colors.green
300
  }
301
  term.setCursorPos( size, 1 )
302
  term.blit( '+', '0', 'e' )
303
  function click( mode, px, py )
304
    while true do
305
      inputWrite( 'Click pos', 1 )
306
      local _, _, x, y = os.pullEvent( 'mouse_click' )
307
      if mode == 'setup' and x == size and y == 1 then
308
        return 'done'
309
      elseif not mode and x == size and y == 1 then
310
        return button()
311
      elseif display.board.large then
312
        x = math.floor( x / 2 )
313
        y = math.floor( y / 2 )
314
      else
315
        x = x - 1
316
        y = y - 1
317
      end
318
      local valid = validClick( x, y, mode, px, py )
319
      if valid == 'cancel' then
320
        return 'cancel'
321
      elseif valid then
322
        return x, y
323
      end
324
    end
325
  end
326
else
327
  names.red  = 'white'
328
  names.blue = 'black'
329
  color = {
330
    red        = colors.white,
331
    redSelect  = colors.white,
332
    blue       = colors.black,
333
    blueSelect = colors.black,
334
    textSelect = colors.gray,
335
    lake       = colors.lightGray,
336
    lakeBg     = colors.black,
337
    pos        = colors.lightGray,
338
    neg        = colors.gray
339
  }
340
  function click( mode, px, py )
341
    while true do
342
      local x, y
343
      while true do
344
        local data = input( 'Pos: ' )
345
        if mode == 'setup' and data == 'done' then
346
          return 'done'
347
        elseif not mode and ( data == 'menu' or data == 'button' ) then
348
          return button()
349
        end
350
        x, y = data:match( '(%d+),%s*(%d+)' )
351
        if x and y then
352
          break
353
        else
354
          writeLog( 'Invalid location' )
355
        end
356
      end
357
      x = tonumber( x )
358
      y = tonumber( y )
359
      local valid = validClick( x, y, mode, px, py )
360
      if valid == 'cancel' then
361
        return 'cancel'
362
      elseif valid then
363
        return x, y
364
      end
365
    end
366
  end
367
end
368
369
-- override defaults with any configured names
370
if config.names then
371
  for k in pairs( config.names ) do
372
    names[k] = config.names[k]
373
  end
374
end
375
376
-- and colors
377
if config.color then
378
  for k in pairs( config.color ) do
379
    color[k] = colors[config.color[k]]
380
  end
381
end
382
383
-- table which converts colors.n into a hexidecimal value to be used by term.blit
384
local hexColor = {}
385
for n=1,16 do
386
    hexColor[2^(n-1)] = string.sub( "0123456789abcdef", n, n )
387
end
388
389
-- data to draw the board
390
local boardData
391
if display.board.large then
392
  local pos = string.rep( hexColor[color.pos], 2 )
393
  local neg = string.rep( hexColor[color.neg], 2 )
394
  boardData = {
395
    row  = '                    ',
396
    pos = string.rep( pos .. neg, 5 ),
397
    neg = string.rep( neg .. pos, 5 ),
398
    size = { 20, 16 }
399
  }
400
else
401
  local pos = hexColor[color.pos]
402
  local neg = hexColor[color.neg]
403
  boardData = {
404
    row = '          ',
405
    pos = string.rep( pos .. neg, 5 ),
406
    neg = string.rep( neg .. pos, 5 ),
407
    size = { 10, 8 }
408
  }
409
end
410
411
-- draw the board using the data from above
412
-- note that "row" is an string of just spaces
413
local board = window.create( term.current(), display.board[1], display.board[2], boardData.size[1], boardData.size[2] )
414
board.setCursorPos( 1, 1 )
415
if display.board.large then
416
  board.blit( boardData.row, boardData.pos, boardData.pos ) board.setCursorPos( 1, 2 )
417
  board.blit( boardData.row, boardData.pos, boardData.pos ) board.setCursorPos( 1, 3 )
418
  board.blit( boardData.row, boardData.neg, boardData.neg ) board.setCursorPos( 1, 4 )
419
  board.blit( boardData.row, boardData.neg, boardData.neg ) board.setCursorPos( 1, 5 )
420
  board.blit( boardData.row, boardData.pos, boardData.pos ) board.setCursorPos( 1, 6 )
421
  board.blit( boardData.row, boardData.pos, boardData.pos ) board.setCursorPos( 1, 7 )
422
  board.blit( boardData.row, boardData.neg, boardData.neg ) board.setCursorPos( 1, 8 )
423
  board.blit( boardData.row, boardData.neg, boardData.neg ) board.setCursorPos( 1, 9 )
424
  board.blit( boardData.row, boardData.pos, boardData.pos ) board.setCursorPos( 1, 10 )
425
  board.blit( boardData.row, boardData.pos, boardData.pos ) board.setCursorPos( 1, 11 )
426
  board.blit( boardData.row, boardData.neg, boardData.neg ) board.setCursorPos( 1, 12 )
427
  board.blit( boardData.row, boardData.neg, boardData.neg ) board.setCursorPos( 1, 13 )
428
  board.blit( boardData.row, boardData.pos, boardData.pos ) board.setCursorPos( 1, 14 )
429
  board.blit( boardData.row, boardData.pos, boardData.pos ) board.setCursorPos( 1, 15 )
430
  board.blit( boardData.row, boardData.neg, boardData.neg ) board.setCursorPos( 1, 16 )
431
  board.blit( boardData.row, boardData.neg, boardData.neg )
432
else
433
  board.blit( boardData.row, boardData.pos, boardData.pos ) board.setCursorPos( 1, 2 )
434
  board.blit( boardData.row, boardData.neg, boardData.neg ) board.setCursorPos( 1, 3 )
435
  board.blit( boardData.row, boardData.pos, boardData.pos ) board.setCursorPos( 1, 4 )
436
  board.blit( boardData.row, boardData.neg, boardData.neg ) board.setCursorPos( 1, 5 )
437
  board.blit( boardData.row, boardData.pos, boardData.pos ) board.setCursorPos( 1, 6 )
438
  board.blit( boardData.row, boardData.neg, boardData.neg ) board.setCursorPos( 1, 7 )
439
  board.blit( boardData.row, boardData.pos, boardData.pos ) board.setCursorPos( 1, 8 )
440
  board.blit( boardData.row, boardData.neg, boardData.neg )
441
end
442
443
-- The file is the second parameter, but only use it if the third is also set
444
-- as otherwise the second parameter is the host name
445
local fileName = ( args[3] and args[2] or config.file )
446
-- and apply the optional file extension from the config (don't apply it if the filename is nil)
447
fileName = fileName and fileName .. ( config.ext or '' )
448
local file = {}
449
-- if the file is set does not exist, skip it
450
if fileName and not fs.exists( fileName ) then
451
  writeLog( 'Save file does not exist' )
452
  fileName = nil
453
-- otherwise load the file
454
elseif fileName then
455
  local f = fs.open( fileName, 'r' )
456
  local data = textutils.unserialize( f.readAll() )
457
  f.close()
458
  -- validate the date
459
  if type( data ) == 'table' then
460
    writeLog( 'Loaded save file ' .. fileName )
461
    file = data
462
  else
463
    writeLog( 'Invalid save file' )
464
    fileName = nil
465
  end
466
end
467
468
local friend, host
469
local name = args[3] or args[2]
470
-- if we are the host, load options from the file and host the game on rednet
471
if args[1] == 'host' then
472
  host = true
473
  options = file.options or {}
474
  writeLog( 'Hosting game with name "' .. name .. '"' )
475
  rednet.host( 'stratego', name )
476
  writeLog( 'Waiting for a friend. ' .. ( isColor and 'Click the + to cancel' or 'Press "end" to cancel' ) )
477
  while true do
478
    -- wait for either a message from the guest, or a prompt from the user to cancel
479
    local event, data, x, y = os.pullEvent()
480
    -- a message means start the game, so send the options to the other player
481
    if event == 'rednet_message' and y == 'stratego' then
482
      writeLog( 'Friend found, sending options' )
483
      -- store the ID for later
484
      friend = data
485
      rednet.send( friend, options, 'stratego-options' )
486
      writeLog( 'Starting game' )
487
      break
488
    -- user's prompt (click on advance systems and "end" on normal ones) means cancel
489
    elseif isColor and ( event == 'mouse_click' and x == size and y == 1 ) or ( event == 'key' and data == keys['end'] ) then
490
      shutdown( 'Canceled game' )
491
      return
492
    end
493
  end
494
  -- the host is always red, and it helps to have these for later
495
  myTeam = 'red'
496
  notMyTeam = 'blue'
497
else
498
  -- if we are the guest, loop up the host
499
  writeLog( 'Looking up host ' .. name )
500
  local ID = rednet.lookup( 'stratego', name )
501
  if ID then
502
    -- once we find them, send a blank message so they get our ID
503
    writeLog( 'Found host with an ID of ' .. ID )
504
    -- store the ID of the host for later
505
    friend = ID
506
    rednet.send( friend, '', 'stratego' )
507
    writeLog( 'Waiting for options' )
508
    while true do
509
      -- then wait for the options
510
      local ID, data = rednet.receive( 'stratego-options' )
511
      if ID == friend then
512
        options = data
513
        break
514
      end
515
    end
516
    writeLog( 'Starting game' )
517
  else
518
    -- if it times out, shutdown the game
519
    shutdown( 'Host name ' .. name .. ' not found on network' )
520
    return
521
  end
522
  -- the guest is always blue, and it helps to have these for later
523
  myTeam = 'blue'
524
  notMyTeam = 'red'
525
end
526
527
-- define the rows for the setup click
528
rows = math.min( options.rows or 3, ( options.rotate and 5 or 4 ) )
529
530
-- how to draw large pieces
531
local largePieces = {
532
  ['*'] = { '\\ ', '**' }, -- bomb
533
  ['!'] = { '^^',  '10' }, -- 10
534
    S   = { '^^',  'S ' }, -- spy
535
    F   = { '|>',  '| ' }, -- flag
536
  
537
  ['?'] = { '??',  '??' }, -- unknown opponent
538
  
539
  [' '] = { '  ',  '  ' }, -- empty space
540
  ['~'] = { '~~',  '~~' }  -- lake
541
}
542
543
-- function to draw a piece
544
function writePiece( x, y, team, piece, sel )
545
  -- if a piece is given, then add it to the board
546
  if piece then
547
    pieces[y][x] = { team, piece }
548
    -- set the text to the team color, or the selection color if we are selecting
549
    board.setTextColor( sel and color.textSelect or color[team] )
550
    -- but don't show us the piece if it's an opponent
551
    if team == notMyTeam then
552
      piece = '?'
553
    end
554
  -- otherwise clear the space
555
  else
556
    pieces[y][x] = nil
557
    piece = ' '
558
  end
559
  -- if the piece is a lake (used on startup), set the background as the lake color
560
  if team == 'lake' then
561
    board.setBackgroundColor( color.lakeBg )
562
  -- if we are selecting it, set the background color as the team color
563
  elseif sel then
564
    board.setBackgroundColor( color[team .. 'Select'] )
565
  -- otherwise apply the "pos" and "neg" colors in a checkerboard
566
  elseif ( x + y ) % 2 == 0 then
567
    board.setBackgroundColor( color.pos )
568
  else
569
    board.setBackgroundColor( color.neg )
570
  end
571
  -- if we are using a large board, use double sized pieces
572
  if display.board.large then
573
    local draw = largePieces[piece] or { '^^', ' ' .. piece }
574
    -- and double the draw coordinates
575
    x = ( x * 2 ) - 1
576
    y = ( y * 2 ) - 1
577
    board.setCursorPos( x, y )
578
    board.write( draw[1] )
579
    board.setCursorPos( x, y + 1 )
580
    board.write( draw[2] )
581
  else
582
    -- otherwise just write as is
583
    board.setCursorPos( x, y )
584
    board.write( piece )
585
  end
586
  -- for some reason the text color gets used by term later...
587
  term.setTextColor( colors.white )
588
end
589
590
-- where to place lakes, in pairs
591
local lakes = options.lakes or {
592
  x = { 3, 4, 7, 8, 3, 4, 7, 8 },
593
  y = { 4, 4, 4, 4, 5, 5, 5, 5 }
594
}
595
-- place the lakes
596
for i, x in pairs( lakes.x ) do
597
  writePiece( x, lakes.y[i], 'lake', '~' )
598
end
599
600
-- function to save a game
601
local turn = options.blueFirst and 'blue' or 'red'
602
function save()
603
  -- we are running a loop inside a loop, so use a variable to break this one
604
  local finished
605
  while not finished do
606
    -- prompt for the file name to save at
607
    inputWrite( 'Filename' )
608
    local data = input() .. config.ext or ''
609
    -- we are finished unless tole otherwise later
610
    finished = true
611
    -- if it exists, prompt to overwrite
612
    if fs.exists( data ) then
613
      inputWrite( 'Overwrite?' )
614
      while true do
615
        local data = input( '(Y/N): ' ):upper()
616
        -- if we choose to overwrite, we are done
617
        if data == 'Y' then
618
          break
619
        -- otherwise restart
620
        elseif data == 'N' then
621
          finished = false
622
          break
623
        end
624
      end
625
    end
626
    -- only run this stuff if we choose to overwrite or no conflict existed
627
    if finished then
628
      local f = fs.open( data, 'w' )
629
      f.write( textutils.serialize( { options = { rotate = options.rotate }, turn = turn, pieces = pieces } ) )
630
      f.close()
631
    end
632
  end
633
end
634
635
-- function for when the button is pressed
636
function button()
637
  while true do
638
    -- prompt for options
639
    local data = input( 'Menu: ' ):lower()
640
    -- "save" means we save the game, then go right back into it
641
    if host and ( data == 'save' or data == 's' ) then
642
      save()
643
      return click()
644
    -- "quit" will prompt for saving if you are the host, then will end the game
645
    elseif data == 'quit' or data == 'q' then
646
      if host then
647
        while true do
648
          local data = input( 'Save? ' ):lower()
649
          if data == 'yes' or data == 'y' then
650
            save()
651
            return 'save'
652
          elseif data == 'no' or 'n' then
653
            break
654
          end
655
        end
656
      end
657
      return 'quit'
658
    -- "forfeit" will cause you to be the loser, needed if you run out of moves
659
    elseif data == 'forfeit' or data == 'f' then
660
      return 'forfeit'
661
    -- a blank call means close the menu
662
    elseif data:gsub( ' ', '' ) == '' then
663
      return click()
664
    end
665
  end
666
end
667
668
-- some pieces are not numerical, so give them numerical ranks
669
-- note the flag has no battle value, as it just overrides things later
670
local battleValues = {
671
  ['*'] = 11,
672
  ['!'] = 10,
673
  S = 0
674
}
675
676
local done, winner, canSpot
677
-- function for when two pieces battle
678
function battle( attacker, defender )
679
  local team1 = attacker[1]
680
  local team2 = defender[1]
681
  local attacker = attacker[2]
682
  local defender = defender[2]
683
  -- if the flag was captured, set victory to our team
684
  if defender == 'F' then
685
    writeLog( 'The ' .. names[team2] .. ' flag has been captured' )
686
    done = true
687
    winner = team1
688
    return 'victory'
689
  -- miners defuse bombs
690
  elseif attacker == 3 and defender == '*' then
691
    writeLog( 'The ' .. names[team1] .. ' 3 has defused the ' .. names[team2] .. ' bomb' )
692
    return true
693
  -- the spy can kill the 10, but only as the attacker
694
  elseif attacker == 'S' and defender == '!' then
695
    writeLog( 'The ' .. names[team1] .. ' spy has killed the ' .. names[team2] .. ' 10' )
696
    return true
697
  else
698
    local attackerName = names[attacker] or attacker
699
    local defenderName = names[defender] or defender
700
    local attacker = battleValues[attacker] or attacker
701
    local defender = battleValues[defender] or defender
702
    if attacker == defender then
703
      writeLog( 'The ' .. names[team1] .. ' ' .. attackerName .. ' has met his match, both pieces were killed' )
704
      return 'tie'
705
    elseif attacker > defender then
706
      writeLog( 'The ' .. names[team1] .. ' ' .. attackerName .. ' killed the ' .. names[team2] .. ' ' .. defenderName )
707
      return true
708
    elseif attacker < defender then
709
      writeLog( 'The ' .. names[team1] .. ' ' .. attackerName .. ' was killed by the ' .. names[team2] .. ' ' .. defenderName )
710
      return false
711
    end
712
  end
713
end
714
715
-- a list of all valid piece calls. Used for the spotter and game setup
716
local validPiece = {
717
  S     = 'S', SPY  = 'S',
718
  F     = 'F', FLAG = 'F',
719
  ['*'] = '*', BOMB = '*', B = '*',
720
  ['!'] = '!', [10] = '!',
721
  [9] = 9,
722
  [8] = 8,
723
  [7] = 7,
724
  [6] = 6,
725
  [5] = 5,
726
  [4] = 4,
727
  [3] = 3,
728
  [2] = 2,
729
  [1] = 1, SPOTTER = 1
730
}
731
-- function for an attack with the spotter
732
function spotter( x, y, override )
733
  local team = pieces[y][x][1]
734
  local rotate = options.rotate
735
  local y2 = rotate and y or ( y + ( team == 'red' and -1 or 1 ) )
736
  local x2 = rotate and ( x + ( team == 'red' and 1 or -1 ) ) or x
737
  local defender = pieces[y2][x2]
738
  if not defender or defender[1] ~= ( team == 'red' and 'blue' or 'red' ) then
739
    return false
740
  end
741
  writeLog( 'The ' .. names[team] .. ' spotter is attempting to snipe the piece at ' .. x2 .. ',' .. y2 )
742
  while true do
743
    local call
744
    if override then
745
      call = override
746
    else
747
      inputWrite( 'Spotter' )
748
      call = input( 'Call: ' )
749
      if call:gsub( ' ', '' ) == '' then
750
        return false
751
      elseif tonumber( call ) then
752
        call = tonumber( call )
753
      else
754
        call = call:upper()
755
      end
756
      call = validPiece[call]
757
    end
758
    if call then
759
      local notteam = ( team == 'red' and 'blue' or 'red' )
760
      if defender[2] == call then
761
        writeLog( 'The ' .. names[team] .. ' spotter has sniped the ' .. names[notteam] .. ' ' .. ( names[defender[2]] or defender[2] ) )
762
        if defender[2] == 'F' then
763
          done = true
764
          winner = team
765
        end
766
        writePiece( x2, y2 )
767
      else
768
        writeLog( 'The ' .. names[team] .. ' spotter failed to snipe the ' .. names[notteam] .. ' ' .. ( names[defender[2]] or defender[2] ) )
769
      end
770
      return call
771
    else
772
      writeLog( 'Invalid call, recall' )
773
    end
774
  end
775
end
776
777
-- function to move a piece
778
function move( x1, y1, x2, y2 )
779
  local piece = pieces[y1][x1]
780
  -- scout movement
781
  if not piece then
782
    writeLog( 'No piece' )
783
  elseif piece[2] == 2 then
784
    -- check that the scout only moved in one direction
785
    if not (( x1 == x2 ) or ( y1 == y2 )) then
786
      writeLog( 'Invalid move' )
787
      return false
788
    else
789
      -- and that no pieces or lakes are in the way
790
      -- we use the temporary variables "c1" and "c2" to make it easier to loop through x or y
791
      local c1, c2, s
792
      if x1 == x2 then
793
        c1 = math.min( y1, y2 )
794
        c2 = math.max( y1, y2 )
795
      else
796
        c1 = math.min( x1, x2 )
797
        c2 = math.max( x1, x2 )
798
      end
799
      for c = c1, c2 do
800
        local piece
801
        -- ignore the start and end points
802
        if c == c1 or c == c2 then
803
          piece = false
804
        -- and check again of if we are using x or y, as the c variables don't keep track of which one
805
        elseif x1 == x2 then
806
          piece = pieces[c][x1]
807
        else
808
          piece = pieces[y1][c]
809
        end
810
        if piece then
811
          writeLog( 'Invalid move' )
812
          return false
813
        end
814
      end        
815
    end
816
  -- for everyone else, check that we only moved one space
817
  elseif math.abs( x2 - x1 ) + math.abs( y2 - y1 ) ~= 1 then
818
    writeLog( 'Invalid move' )
819
    return false
820
  end
821
  -- state the action, but make sure not to state the piece rank
822
  writeLog( names[piece[1]]:gsub( '^%l', string.upper ) .. ' moved from ' .. x1 .. ',' .. y1 .. ' to ' .. x2 .. ',' .. y2 )
823
  local destination = pieces[y2][x2]
824
  local result
825
  -- if someone already exists at the target, then battle
826
  if destination then
827
    result = battle( piece, destination )
828
  end
829
  -- if the battle was a tie, kill the target space's piece
830
  if result == 'tie' then
831
    writePiece( x2, y2 )
832
  -- otherwise if we won or there was no battle, place the current piece at the target
833
  elseif result or not destination then
834
    writePiece( x2, y2, piece[1], piece[2] )
835
  end
836
  -- clear the current space anyways and return success
837
  writePiece( x1, y1 )
838
  return result or true
839
end
840
841
local maxPieces = options.maxPieces or {
842
  S = 1, F = 1, ['!'] = 1, [9] = 1, [8] = 1, [7] = 1,
843
  [6] = 2, [5] = 2, [4] = 2, [1] = 2,
844
  [3] = 5, [2] = 5,
845
  ['*'] = 6
846
}
847
-- set maximums for pieces and flags
848
local maxRequiredPieces = rows * ( options.rotate and 8 or 10 )
849
local requiredPieces = math.min( options.requiredPieces or maxRequiredPieces, maxRequiredPieces )
850
local requiredFlags = math.min( options.requiredFlags or 1, 10 )
851
852
-- called with a file to load the file or run by both side to load piece setup
853
function setup()
854
  writeLog( 'Place pieces in the first ' .. rows .. ( options.rotate and ' columns. ' or ' rows. ' ) .. ( isColor and 'Press the + when finished' or 'Type "done" when finished' ) )
855
  -- tell the user how many flags are required, normally is just 1
856
  local flagText
857
  if requiredFlags > 0 then
858
    flagText = requiredFlags .. ( requiredFlags == 1 and ' flag is required' or ' flags are required' )
859
    writeLog( flagText )
860
  end
861
  -- keep track of pieces for the sake of the "done" button and not placing too many
862
  local usedPieces = {}
863
  local placedPieces = 0
864
  inputWrite( 'Place pieces' )
865
  while true do
866
    -- wait for a space to be clicked
867
    local x,y = click( 'setup' )
868
    -- if the user clicked the done button, check if they placed enough pieces and flags
869
    if x == 'done' then
870
      if placedPieces == requiredPieces then
871
        if ( usedPieces.F or 0 ) < requiredFlags then
872
          while true do
873
            -- if they are not equal, loop until the user does something other than press the done button
874
            writeLog( flagText )
875
            x,y = click( 'setup' )
876
            if x ~= 'done' then
877
              break
878
            end
879
          end
880
        else
881
          inputClear()
882
          inputClear(1)
883
          inputClear(2)
884
          break
885
        end
886
      else
887
        while true do
888
          -- if there is not enough, loop until the user does something other than press the done button
889
          writeLog( 'Not enough pieces placed' )
890
          x,y = click( 'setup' )
891
          if x ~= 'done' then
892
            break
893
          end
894
        end
895
      end
896
    end
897
    -- show selected space
898
    inputWrite( x .. ',' .. y, 2 )
899
    -- if a piece is already there, remove it from the count (we will remove it from the board with the selection later
900
    if pieces[y][x] then
901
      local piece = pieces[y][x][2]
902
      usedPieces[piece] = usedPieces[piece] - 1
903
      placedPieces = placedPieces - 1
904
    end
905
    -- don't allow a piece to be placed if we are out of pieces
906
    if placedPieces == requiredPieces then
907
      writeLog( 'Out of pieces' )
908
    else
909
      -- select the space
910
      writePiece( x, y, myTeam, nil, true )
911
      -- tell the user how many pieces remain
912
      -- TODO: make it display properly on smaller screens
913
      writeLog( 'Available: (place ' .. requiredPieces - placedPieces .. ' more)' )
914
      for k, v in pairs( maxPieces ) do
915
        local remaining = v - ( usedPieces[k] or 0 )
916
        if remaining ~= 0 then
917
          writeLog( '* ' .. ( names[k] or k ) .. ': ' .. remaining )
918
        end
919
      end
920
      while true do
921
        -- if we passed all other steps, ask the user what piece to place
922
        local data = input( 'Piece: ' )
923
        -- turn strings into numbers or uppercase (as that is how it's stored
924
        if tonumber( data ) then
925
          data = tonumber( data )
926
        else
927
          data = data:upper()
928
        end
929
        -- if the piece is valid
930
        local piece = validPiece[data]
931
        if piece then
932
          -- if we are out of the piece, ask the user to type the name again
933
          if usedPieces[piece] == maxPieces[piece] then
934
            writeLog( 'Out of ' .. ( names[piece] or piece ) .. "s" )
935
          else
936
            -- otherwise place the piece and increase the counters for that piece and total pieces
937
            writePiece( x, y, myTeam, piece )
938
            usedPieces[piece] = ( usedPieces[piece] or 0 ) + 1
939
            placedPieces = placedPieces + 1
940
            break
941
          end
942
        -- if the input is blank or only contains spaces, then just remove the selection
943
        elseif data:gsub( ' ', '' ) == '' then
944
          writePiece( x, y )
945
          break
946
        else
947
          writeLog( 'Invalid piece' )
948
        end
949
      end
950
      inputClear(2)
951
    end
952
  end
953
  -- once done, host the name under "stratego-done" so the other user sees it upon finishing
954
  writeLog( 'Waiting for other player' )
955
  rednet.host( 'stratego-done', name .. ( host and 1 or 0 ) )
956
  while true do
957
    local found = rednet.lookup( 'stratego-done', name .. ( host and 0 or 1 ) )
958
    if found == friend then
959
      -- once we find the other user is done, start sending pieces back and fourth
960
      if not host then
961
        -- the guest sends its pieces to the host
962
        writeLog( 'Sending pieces to host' )
963
        os.sleep( 2 ) -- delay to compensate for rednet.lookup
964
        rednet.send( friend, pieces, 'stratego-pieces-guest' )
965
        writeLog( 'Waiting for combined pieces' )
966
        -- then waits to get the host's combination
967
        while true do
968
          local ID, data = rednet.receive( 'stratego-pieces-host' )
969
          if ID == friend then
970
            writeLog( 'Received pieces from host' )
971
            pieces = data
972
            break
973
          end
974
        end
975
      else
976
        -- the host waits to receive the guest's pieces
977
        writeLog( 'Waiting for pieces from guest' )
978
        while true do
979
          local ID, data = rednet.receive( 'stratego-pieces-guest' )
980
          if ID == friend then
981
            writeLog( 'Received pieces from guest' )
982
            -- then combines the two and sends them back to the guest
983
            -- if for some reason a value exists in both the guest and host, the host's piece is kept
984
            -- though this should never happen except for lakes (which are in the same spots on both sides already)
985
            for x = 1, 10 do
986
              for y = 1, 8 do
987
                pieces[y][x] = pieces[y][x] or data[y][x]
988
              end
989
            end
990
            writeLog( 'Sending combined pieces to guest' )
991
            rednet.send( friend, pieces, 'stratego-pieces-host' )
992
            break
993
          end
994
        end
995
      end
996
      -- wait to unhost until after we finished everything, as otherwise lag can cause both systems to be stuck waiting
997
      rednet.unhost( 'stratego-done', name .. ( host and 1 or 0 ) )
998
      break
999
    end
1000
  end
1001
  writeLog( 'Setup complete, starting game' )  
1002
end
1003
1004
-- startup
1005
if host then
1006
  -- if we loaded a file and it contains pieces, load it instead of running setup
1007
  if file and file.pieces then
1008
    rednet.send( friend, file, 'stratego-setup' )
1009
    pieces = file.pieces
1010
    turn = file.turn or turn
1011
  else
1012
    -- otherwise send a message to the guest indicating we are running setup
1013
    -- a blank string is used, since the guest uses the type of data to determine the action
1014
    rednet.send( friend, '', 'stratego-setup' )
1015
    setup()
1016
  end
1017
else
1018
  -- wait for the host to tell us whether we already have pieces, or still need to load them
1019
  while true do
1020
    local ID, file = rednet.receive( 'stratego-setup' )
1021
    if ID == friend then
1022
      -- if the file is a table, we know it is the pieces
1023
      if type( file ) == 'table' then
1024
        writeLog( 'Loading pieces from host' )
1025
        pieces = file.pieces
1026
        turn = file.turn or turn
1027
      else
1028
        setup()
1029
      end
1030
      break
1031
    end
1032
  end
1033
end
1034
1035
-- after setting up, write all pieces to the board
1036
-- as we either loaded setup pieces from the guest/host, or from a file
1037
for y = 1, 8 do
1038
  for x = 1, 10 do
1039
    local piece = pieces[y][x]
1040
    -- if we don't have a piece, we need to draw a blank space to make sure any improperly added pieces are overwritten
1041
    -- shouldn't happen, but just in case
1042
    if piece then
1043
      writePiece( x, y, pieces[y][x][1], pieces[y][x][2] )
1044
    else
1045
      writePiece( x, y, nil, nil )
1046
    end
1047
  end
1048
end
1049
1050
-- main loop, using the variable "done" to determine completion (as we use multiple loops inside)
1051
while not done do
1052
  -- if its our turn
1053
  if turn == myTeam then
1054
    -- collecting data of the turn
1055
    local act = {}
1056
    inputWrite( 'Move piece' )
1057
    while true do
1058
      while true do
1059
        local x, y = click()
1060
        -- if we choose to quit, then quit the game and end the program
1061
        if x == 'quit' or x == 'save' then
1062
          rednet.send( friend, 'quit', 'stratego-action' )
1063
          if x == 'save' then
1064
            shutdown( 'Game saved and quit' )
1065
          else
1066
            shutdown( 'Quit game' )
1067
          end
1068
          return
1069
        -- if we choose to forfeit, then set the winner to the opponent and break the loop
1070
        elseif x == 'forfeit' then
1071
          act[1] = 'forfeit'
1072
          writeLog( 'You forfeited' )
1073
          winner = notMyTeam
1074
          done = true
1075
          break
1076
        -- if the piece cannot move, don't select it
1077
        elseif  ( y + 1 >  8 or ( pieces[y + 1][x] and pieces[y + 1][x][1] ~= notMyTeam ) )
1078
        and ( y - 1 <  1 or ( pieces[y - 1][x] and pieces[y - 1][x][1] ~= notMyTeam ) )
1079
        and ( x + 1 > 10 or ( pieces[y][x + 1] and pieces[y][x + 1][1] ~= notMyTeam ) )
1080
        and ( x - 1 <  1 or ( pieces[y][x - 1] and pieces[y][x - 1][1] ~= notMyTeam ) ) then
1081
          writeLog( 'Piece cannot move' )
1082
        -- otherwise select it
1083
        else
1084
          -- display selection
1085
          inputWrite( x .. ',' .. y, 2 )
1086
          writePiece( x, y, myTeam, pieces[y][x][2], true )
1087
          -- and set the data for later use
1088
          act[1] = x
1089
          act[2] = y
1090
          break
1091
        end
1092
      end
1093
      -- if we forfeited, our turn is over
1094
      if act[1] == 'forfeit' then
1095
        break
1096
      -- otherwise if we are a spotter, run the spotter function
1097
      elseif pieces[act[2]][act[1]][2] == 1 then
1098
        act[3] = spotter( act[1], act[2] )
1099
      end
1100
      -- if the spotter could spot and spotted, then the turn is done
1101
      if act[3] then
1102
        -- deselect the piece
1103
        writePiece( act[1], act[2], myTeam, 1 )
1104
        break
1105
      -- otherwise proceed with movement
1106
      else
1107
        local canSpot
1108
        while true do
1109
          -- another click, but this time check for deselection
1110
          local x, y = click( 'attack', act[1], act[2] )
1111
          -- if we cancel, deselect the piece and go back to the first loop
1112
          if x == 'cancel' then
1113
            inputClear(2)
1114
            writePiece( act[1], act[2], myTeam, pieces[act[2]][act[1]][2] )
1115
            break
1116
          -- need to check if we can spot before moving, as the target data gets replaced later on
1117
          elseif not pieces[y][x] and pieces[act[2]][act[1]][2] == 1 then
1118
            canSpot = true
1119
          else
1120
            canSpot = false
1121
          end
1122
          -- move the piece
1123
          local success = move( act[1], act[2], x, y )
1124
          -- if the move was successful, set the data, otherwise we retry the click
1125
          if success then
1126
            act[3] = x
1127
            act[4] = y
1128
            break
1129
          end
1130
          -- no need to use the spotter's ability if we just won
1131
          if success == 'victory' then
1132
            canSpot = false
1133
          end
1134
        end
1135
        -- if we can spot and successfully moved, then run the spotter
1136
        if canSpot and act[3] and pieces[act[4]][act[3]] then
1137
          act[5] = spotter( act[3], act[4] )
1138
          break
1139
        -- otherwise just stop if we successfully moved, or restart the whole loop if not
1140
        elseif act[3] then
1141
          break
1142
        end
1143
      end
1144
    end
1145
    -- turn is over, so send the results to the other player and set the turn to them
1146
    rednet.send( friend, act, 'stratego-action' )
1147
    turn = notMyTeam
1148
  else
1149
    --if it's not our turn, wait for information on the opponents turn
1150
    inputWrite( 'Waiting...' )
1151
    inputClear(1)
1152
    inputClear(2)
1153
    while true do
1154
      local ID, act = rednet.receive( 'stratego-action' )
1155
      if ID == friend then
1156
        -- found information
1157
        -- if the opponent choose to quit, then quit as well
1158
        if act == 'quit' then
1159
          -- and prompt to save the game if we are the host
1160
          if host then
1161
            writeLog( 'Game quit by guest' )
1162
            inputWrite( 'Save game?' )
1163
            while true do
1164
              local data = input( '(Y/N): ' ):upper()
1165
              if data == 'Y' then
1166
                save()
1167
                break
1168
              elseif data == 'N' then
1169
                break
1170
              end
1171
            end
1172
            shutdown()
1173
          else
1174
            shutdown( 'Game quit by host' )
1175
          end
1176
          return
1177
        -- if the opponent forfeited, then set the winner to us, tell the player, and exit the main loop
1178
        elseif act[1] == 'forfeit' then
1179
          winner = myTeam
1180
          writeLog( 'Opponent forfeited' )
1181
          done = true
1182
        -- otherwise check if the opponent used a spotter, and if so run the spotter function
1183
        elseif not act[4] and pieces[act[2]][act[1]][2] == 1 then
1184
          spotter( act[1], act[2], act[3] )
1185
        -- otherwise, assume a movement
1186
        else
1187
          -- need to check if we can spot before moving, as the target data gets replaced later on
1188
          -- this is mainly an anti-cheat thing, to prevent a player from both moving and spotting
1189
          local canSpot
1190
          if not pieces[act[4]][act[3]] and pieces[act[2]][act[1]][2] == 1 then
1191
            canSpot = true
1192
          end
1193
          -- move the piece, then if we can spot, run the spotter function
1194
          move( act[1], act[2], act[3], act[4] )
1195
          if canSpot and act[5] then
1196
            spotter( act[3], act[4], act[5] )
1197
          end
1198
        end
1199
        -- since we found something, break out of the loop
1200
        break
1201
      end
1202
    end
1203
    -- and it's our turn next time
1204
    turn = myTeam
1205
  end
1206
end
1207
1208
-- at the end of the game, state the winner then delay a bit for both sides to read it
1209
writeLog( winner:gsub( '^%l', string.upper ) .. ' is the winner' )
1210
os.sleep( 5 )
1211
1212
-- if we are the host and the file is a save game, prompt to delete it
1213
if host and fileName and file.pieces then
1214
  inputWrite( 'Delete game?' )
1215
  while true do
1216
    local data = input( '(Y/N): ' ):upper()
1217
    if data == 'Y' then
1218
      fs.delete( fileName )
1219
      break
1220
    elseif data == 'N' then
1221
      break
1222
    end
1223
  end
1224
end
1225
1226
-- and final shutdown stuff
1227
shutdown()