Игры для командной строки windows

Привет!

Сегодня я опишу в подробностях, как я сделал игру в командной строке, и насколько хороша она получилась.

Откуда идея?

Я вдохновился идеей сделать что-то простое на первый взгляд, но в тоже время интересное в плане разработки. Мне в голову пришла мысль, сделать игру в консоли, это интересно в плане разработки, и посмотреть на это будет интересно даже просто со стороны, как например на эту игру.

Игровой движок

Итак, начнем с того как игра устроена в корне, и какова ее идея работы.

Сначала я определился с тем, как будет выводится игровой мир в консоль. Понял, что для вывода игровых объектов, нам нужен список, который хранит в себе другие списки, которые хранят в себе символы, которые в последующем выводятся на игровое поле циклом for.

Вот таким кодом:

for line_words in OUTPUT_IMAGE:
       for word in line_words:
           print(word, end="")
       print("\n", end="")

Здесь мы рисуем все символы из списка, и переходим на новую строку, чтоб нарисовать следующий список символов.

Вот так выглядит переменная, которая хранит списки символов:

image

Тут cразу мы получаем решение как нам выводить по X и Y объекты, мы теперь можем указывать:

X — символ в списке
Y — список в котором содержится X
Тем самым нарисовать на поле какой-нибудь символ. Это мы будем использовать при рисовании игровых объектов.

Можем попробовать нарисовать на поле «мяч», подставив на место X и Y букву «O».

Для этого напишем такой код:

import os
OUTPUT_IMAGE = [
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        ]

OUTPUT_IMAGE[4][6] = "O"
os.system("cls||clear")
for line_words in OUTPUT_IMAGE:
       for word in line_words:
           print(word, end="")
       print("\n", end="")

image

И вот, мы нарисовали объект на нашем игровом поле. Правда координаты X и Y получились не классическими. Во первых, мы указываем сначала Y, потом X, что не совсем по классике, во вторых, координата Y должна увеличиваться чтоб поднять объект, у нас же наоборот, она должна уменьшатся.

График X и Y в игре:

image

Эту особенность тоже придется учитывать в дальнейшем, когда мы будем делать столкновение объектов в консоли.

Теперь мы можем попробовать перемещать наш объект по игровому полю, т.е. создавать движение.

Нам понадобится очищать консоль, для того чтобы стирать старую картинку игрового поля.
Это мы сделаем командой:

os.system("cls||clear")

Также, нам понадобится переопределять переменную OUTPUT_IMAGE, для того чтобы очищать все ранее нарисованные в игровом поле объекты.

Также нам все это нужно будет поместить в while True.

Добавим в while True функцию time.sleep(1), для того чтобы ограничить FPS.

И вот, код нарисовался на глазах:

from time import sleep
from os import system
OUTPUT_IMAGE = [
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
        ]

x = 0
y = 0
while True:
      sleep(1)
      system("cls||clear")
      OUTPUT_IMAGE[y][x] = "O"
      for line_words in OUTPUT_IMAGE:
             for word in line_words:
                 print(word, end="")
             print("\n", end="")
      y += 1
      x += 1
      OUTPUT_IMAGE = [
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            ]

image

Теперь у нас есть возможность распределять объекты по полю.

Правда эти объекты слишком примитивы, и надо бы научится рисовать сложные объекты по типу игроков, домов, еды…

Для того чтобы нарисовать сложный объект, нам нужно понять и придумать, как нарисовать объект указав лишь один раз его X и Y.

Для этого нам понадобится функция, которая принимает картинку (символы), X, Y;

Так и сделаем:

def SetImage(image: str, x: int, y: int):
    pass

Теперь нам нужно ее реализовать. Для этого нужно решить, как нарисовать изображение, которое растягивается по оси X и Y, я придумал так:
рисовать объект разделяя его на символы, и как только встретится символ «\n», прибавить ось Y.

Ось Y как мы уже говорили неправильная, перевернутая наоборот, поэтому к ней мы прибавляем чтобы опустить объект.

Пример изображения который рисуется по моему принципу:

image = " O\n'|'\n |"#игрок

Теперь давайте это опишем в нашей функции:

def SetImage(x: int, y: int, image: str):
  x_start = x
  x = x
  y = y
  for word in image:
      if word == "\n":
          x = x_start
          y += 1
      else:
          x += 1
          try:
            OUTPUT_IMAGE[y][x] = word
          except IndexError:
              break

Добавим try: except() для того чтобы небыло ошибок если объект имеет X и Y слишком мальенькие или слишком большие.

x_start Это X, с которого нужно начинать рисовать при увеличении Y (при символе «\n»)

Теперь мы можем использовать нашу функцию, падать в нее X и Y, и картинку которую нужно рисовать:

код

from time import sleep
from os import system
OUTPUT_IMAGE = [
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      ]

def SetImage(x: int, y: int, image: str):
  x_start = x
  x = x
  y = y
  for word in image:
      if word == "\n":
          x = x_start
          y += 1
      else:
          x += 1
          try:
            OUTPUT_IMAGE[y][x] = word
          except IndexError:
              break
while True:
      sleep(1)
      system("cls||clear")
      SetImage(x=3,y=4,image=" O\n'|'\n |")
      for line_words in OUTPUT_IMAGE:
             for word in line_words:
                 print(word, end="")
             print("\n", end="")
      OUTPUT_IMAGE = [
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            ]

И вот что у нас получилось:

image

абсолютно также как и шарик который мы рисовали, его можно двигать по оси X и Y.

код

from time import sleep
from os import system
OUTPUT_IMAGE = [
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      ]
px = 0
py = 0
def SetImage(x: int, y: int, image: str):
  x_start = x
  x = x
  y = y
  for word in image:
      if word == "\n":
          x = x_start
          y += 1
      else:
          x += 1
          try:
            OUTPUT_IMAGE[y][x] = word
          except IndexError:
              break
while True:
      sleep(1)
      system("cls||clear")
      SetImage(x=px,y=py,image=" O\n'|'\n |")
      for line_words in OUTPUT_IMAGE:
             for word in line_words:
                 print(word, end="")
             print("\n", end="")
      px += 1
      py += 1
      OUTPUT_IMAGE = [
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            ]

image

И вот, у нас уже двигает игрок по карте.

Тут мы сделали уже многое, уже есть игрок, уже есть карта, и казалось бы, уже можно сделать игру, но нет. Нам нужна функция высчета столкновений объектов, ведь какая это игра без взаимодействий объектов. Поэтому приступим.

Для начала нам нужно сделать функцию получение широты и высоты объекта, для того чтобы расчитать его хитбокс.

Итак, функцию я решил сделать по такой логике:

X — хитбокс объекта по X ширине, это самое больше количество символов между знаками «\n» в картинке
Y — хитбокс по Y это число символов «\n» в картинке

По этой логике не сложно сделать функцию, которая принимает картинку, считает у нее все символы между «\n», и выбирает из этого самое больше число символов — получилась широта.
И если посчитать символы «\n», как я уже написал — получится высота.

Функция получилась такой:

def GetSizeObject(img: str):
    w = 0
    weights = []
    h = [word for word in img if word == "\n"]

    for word in img:
      if word == "\n":
          weights.append(w)
          w = 0
      else:
          w += 1
      try:
          return {"w": max(weights), "h":len(h)}
      except ValueError:
            return {"w": 0, "h":0}

Зачем здесь ValueError except?

Он здесь чтобы предотвратить ошибку при запуске игры.

Итак, давайте нарисуем нашего игрока, и вычислил его ширину и длину.

код с рисовкой и вычислением широты и высоты

from time import sleep
from os import system
OUTPUT_IMAGE = [
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      ]
px = 3
py = 3
def SetImage(x: int, y: int, image: str):
    global OUTPUT_IMAGE      
    x_start = x
    x = x
    y = y
    for word in image:
        if word == "\n":
            x = x_start
            y += 1
        else:
            x += 1
            try:
              OUTPUT_IMAGE[y][x] = word
            except IndexError:
                break

def GetSizeObject(img: str):
    w = 0
    weights = []
    h = [word for word in img if word == "\n"]
    h.append(1)

    for word in img:
        if word == "\n":
            weights.append(w)
            w = 0
        else:
            w += 1
    try:
        return {"w": max(weights), "h":len(h)}
    except ValueError:
        return {"w": 0, "h":0}

player_image = " O\n'|'\n |"
def draw():
      global OUTPUT_IMAGE
      sleep(1)
      system("cls||clear")
      for line_words in OUTPUT_IMAGE:
             for word in line_words:
                 print(word, end="")
             print("\n", end="")
      OUTPUT_IMAGE = [
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            ]
while True:
    SetImage(x=px,y=py,image=player_image)
    print(GetSizeObject(img=player_image))
    draw()

Ура! у нас есть функция вычисления широты и высоты, теперь нам предстоит сделать функцию вычисления хитбокса и столкновений объектов.

Вспомним что у нас система координат не классическая, поэтому классическую функцию увы использовать не сможем, придется делать свою. Я для этого я нарисовал на графике 2 квадрата, которые сталкиваются, и по этой картинке можно придумать условие по которому будет высчитано столкновение

Для простоты понимания я нарисовал хитбоксы, Т.Е. квадраты:

image

Логика на словах

Для вычисления мы подаем

x — X первого объекта
y — Y первого объекта
h — Высота первого объекта
w — Широта первого объекта
x2 — X второго объекта
y2 — Y второго объекта
h2 — Высота второго объекта
w2 — Широта второго объекта

И смотрим:

если
y больше y2 - h2 + h и y - h меньше чем y2 + h2 - h
или же
y2 больше y - h + h2 и y2 - h2 меньше чем y + h - h2

Зачем проверять 2 раза?

Мы сделали проверку 2 раза, просто из-за того чтобы посмотреть на столкновение/не столкновение с разных объектов.

Объекты соприкасаются по оси Y

Дальше смотри соприкосновение по оси X, она такое же что и по оси Y, но вместо yx, а вместо hw.

если:

x больше x2 - w2 + w и x - w меньше чем x2 + w2 - w

или же

x2 больше x - w + w2 и x2 - w2 меньше чем x + w - w2

объекты соприкасаются по оси X

Логика в коде

Логика такая же как и на словах, только в функции:

def IsClash(x: int, y: int, h: int, w: int,x2: int, y2: int, h2: int, w2: int):
    if (y >= y2 - h2 + h and y - h <= y2 + h2 - h) or (y2 >= y - h + h2 and y2 - h2 <= y + h - h2):
        if (x >= x2 - w2 + w and x - w <= x2 + w2 - w) or (x2 >= x - w + w2 and x2 - w2 <= x + w - w2):
            return True

    return False

Функция возвращает True если объекты соприкасаются, и False если нет.

Я нарисовал дополнительно куб на нашем игровом поле, для того чтобы игроку было с кем сталкиваться.

И попробовал как работает функция высчета столкновения.

Вот игрок соприкасается и кубом:

image

А вот нет соприкасаются:

image

Код соприкосновения

Это полный код соприкосновения/не соприкосновения:

from time import sleep
from os import system
OUTPUT_IMAGE = [
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
      ]

def SetImage(x: int, y: int, image: str):
    global OUTPUT_IMAGE      
    x_start = x
    x = x
    y = y
    for word in image:
        if word == "\n":
            x = x_start
            y += 1
        else:
            x += 1
            try:
              OUTPUT_IMAGE[y][x] = word
            except IndexError:
                break

def GetSizeObject(img: str):
    w = 0
    weights = []
    h = [word for word in img if word == "\n"]
    h.append(1)

    for word in img:
        if word == "\n":
            weights.append(w)
            w = 0
        else:
            w += 1
    try:
        return {"w": max(weights), "h":len(h)}
    except ValueError:
        return {"w": 0, "h":0}

def IsClash(x: int, y: int, h: int, w: int,x2: int, y2: int, h2: int, w2: int):
    if (y >= y2 - h2 + h and y - h <= y2 + h2 - h) or (y2 >= y - h + h2 and y2 - h2 <= y + h - h2):
        if (x >= x2 - w2 + w and x - w <= x2 + w2 - w) or (x2 >= x - w + w2 and x2 - w2 <= x + w - w2):
            return True

    return False

player_image = " O\n'|'\n |"
cube_image = "____\n|  |\n----"
cx = 5#
cy = 4  #Меняйте эти координаты для того чтобы менять позиции игрока и куба
px = 10  #
py = 3#
def draw():
      global OUTPUT_IMAGE
      sleep(1)
      system("cls||clear")
      for line_words in OUTPUT_IMAGE:
             for word in line_words:
                 print(word, end="")
             print("\n", end="")
      OUTPUT_IMAGE = [
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
            ]
while True:
    SetImage(x=px,y=py,image=player_image)
    SetImage(x=cx,y=cy,image=cube_image)
    print("is clash: ",IsClash(
      x=px,
      x2=cx,
      y=py,
      y2=cy,
      h=GetSizeObject(img=player_image)["h"],
      h2=GetSizeObject(img=cube_image)["h"],
      w=GetSizeObject(img=player_image)["w"],
      w2=GetSizeObject(img=cube_image)["w"],
      ))
    draw()

Теперь у нас все стартовые функции для игры, собственно их основе я писал свою игру.

Игра

Идея игры в такая:

Есть игрок, вокруг появляется еда, которую он вынужден собрать чтоб не умереть. В игре также присутствуют функции: поднять еду, положить в инвентарь, съесть ее из инвентаря, положить на пол предмет из инвентаря

Я начал с того что сделал игровой цикл в 3 строчки, это просто While True:

from time import sleep
while True:
    sleep(0.1)

Дальше я посчитал нужным, создать класс, в котором будут хранится все функции будующий объектов. Поэтому создал файл main.py и папку lib, в которую поместил файл lib.py в котором был класс игры. Т.Е. файлы игры выглядели так:

+----game
|    + -- 
|    | -- main.py
|    \ --lib
|         +--lib.py -> class Game()
|         \
|
+---

В дальнейшем я работал в основном с классом Game(), в main.py просто вызывал его, создавал стартовые объекты, запускал игру.

В классе game сделал функцию run(), которая заупускает игровой цикл. Также сделал функцию draw_all(), она стирает все прошлые объекты, рисует новые, и печатает на игровое поле.

И так выглядел класс:

from time import sleep


class Game():
    def __init__(self):
        self.OUTPUT_IMAGE = []  # здесь игровое поле

    def draw_all(self):
        for line_words in self.OUTPUT_IMAGE:
            for word in line_words:
                print(word, end="")
            print("\n", end="")

    def run(self):
        while True:
            self.draw_all()
            sleep(0.1)

Добавил все основные функции, по типу set_image(), size_object(), is_clash(), и все те которые являются игровым движком, и которые я описал выше.

Сделал новую функцию create_object() и переменную self.OBJECTS, функцию create_object()я использую для создания объектов, она принимает параметры img, name, x, y, up, rigid, data.

img — картинка объекта
name — имя объекта (дом, трава, житель, еда и.т.п.)
x — X объекта
y — Y объекта
up — если этот параметр True, то объект рисуется над игроком, иначе игрок его перекрывает собой
rigid — твердость, игрок не может пройти через этот объект (еще не реализовано)
data — личные данные объекта, его личные характеристики

create_object()

Эта функцию которая сейчас у меня в игре:

def CreateObject(self,x: int, y: int, img: str, name: str = None, up: bool = False, rigid: bool = False, data: dict = {}):
    size_object = self.GetSizeObject(img=img)
    self.OBJECTS.append(
        {"name": name,
         "x": x,
         "y": y,
         "up": up,
         "rigid": rigid,
         "h":size_object["h"],
         "w":size_object["w"],
         "id":uuid4().hex,
         "data":data,
         "img": img}
    )

На тот момент я уже добавил игрока, дом, траву, и жителя.

И решил использовать тот самый параметр в объекте up, использовать его в объекте Home, Т.Е. чтоб дом закрывал собой игрока. Для этого я сделал функцию CheckAll(), циклом for проходился по всем объектам, и рисовал их на исходящей картинке, Т.Е. использовать функцию SetImage(x: int, y: int, img:str), подавая в нее X и Y объекта, и картинку.

Тем самым рисовал объекты которые мог закрыть собой игрок. В этом же цикле я объявил списокup_of_payer_objects, и если у объекта стоял up=True, то я добавлял его в список, не рисуя его на поле. После рисовал самого игрока, и только этого я проходил циклом for по объектам в up_of_payer_objects, рисуя их, тем самым они были над игроком.

def CheckAll(self):
    up_of_payer_objects = []
    for object_now in range(len(self.OBJECTS)):
        if object_now["up"]:
            up_of_payer_objects.append(object_now)
            continue
        self.SetImage(x=object_now["x"],y=object_now["y"],image=object_now["img"])

Дальше я занялся движением игрока. Для этого я создал его как отдельный объект, который не находится в списке self.OBJECTS, но который хранится в переменной self.PLAYER.

Все его параметры, по типу X, Y, img, и.т.п. получить можно с помощью ключей, проще говоря это словарь (dict). С таким игроком и объектами уже можно было работать, двигать, вычислить столкновения. Я начал с движения.
Начал создавать движение с того что сделал функцию CheckKeysObjects(), которая отвечает за отслеживание нажатия клавиш, и которую я вызываю в функции CheckAll() в самом начале

def CheckAll(self):
    self.CheckKeysObjects()
    ....

Для отслеживания нажатий на клавиши я использовал библиотеку keyboard, и 4 переменные:

self.WALK_LEFT_PLAYER
self.WALK_RIGHT_PLAYER
self.WALK_UP_PLAYER
self.WALK_DOWN_PLAYER

И все оказалось просто, отслеживаем клавиши, и если нажата допустим d, то мы переменную self.WALK_RIGHT_PLAYER делаем True.

В самом начале функции объявляем все переменные в False, для того чтобы сбросить все прошлые результаты, а-то игрок не остановится.

CheckKeysObjects()

def CheckKeysObjects(self):
    #делаю все переменные в False, чтоб сбросить прошлые результаты
    self.WALK_LEFT_PLAYER = False
    self.WALK_RIGHT_PLAYER = False
    self.WALK_UP_PLAYER = False
    self.WALK_DOWN_PLAYER = False
    #а тут уже проверяю нажатия
    if keyboard.is_pressed("a"):
        self.WALK_LEFT_PLAYER = True
    elif keyboard.is_pressed("d"):
        self.WALK_RIGHT_PLAYER = True
    if keyboard.is_pressed("w"):
        self.WALK_UP_PLAYER = True
    elif keyboard.is_pressed("s"):
        self.WALK_DOWN_PLAYER = True

После этого я в функции CheckAll() проверяю все перменные отвечающие за движение, узнаю, куда двигается игрок.

Если какая-то в True, узнаем какая, и двигаем предмет в противоположную сторону.

Получившийся код движения

def CheckAll(self):
    self.CheckKeysObjects()  # check moves
    up_of_payer_objects = []
    for object_now in range(len(self.OBJECTS)):
        self.PLAYER["img"] = self.PLAYER["image_normal"]
        if self.WALK_LEFT_PLAYER:
            self.OBJECTS[object_now]["x"] += 1

        elif self.WALK_RIGHT_PLAYER:
            self.OBJECTS[object_now]["x"] -= 1


        if self.WALK_UP_PLAYER:

            self.OBJECTS[object_now]["y"] += 1
        elif self.WALK_DOWN_PLAYER:

            self.OBJECTS[object_now]["y"] -= 1

Да, мы двигаем предметы в противоположную сторону, для того чтобы создать иллюзию движения. Если игрок идет на право, то все предметы окружения смещаются налево.

Дальше я добавил еще предметов окружения, и занялся спавном еды, у игрока цель собирать еду, чтобы не умереть.

Для отсчета времени спавна еды, я использовал простой time.sleep(), и библиотеку threading — для того чтобы запустить 2 функции одновременно, спавн еды и основной игровой цикл. Функция спавна еды SpawnEat() — это просто функция которая при запуске генерирует на случайных местах еду, вызывая для каждой единицы еды функцию CreateObject().

Также, как только я сделал функцию спавна еды, я сделал переменную у игрока self.PLAYER["hungry"], это его голод, в самом начале он равен — 100 ед., его я буду уменьшать если игрок ходит и тратит энегрию (типа энергию, ее в игре нет) или увеличивать если игрок что-то съел.

Также я сделал функцию MinimizeHungry(), она вызывается каждые 5 секунд, и просто отнимает у игрока 2 единицы голода. Это я сделал для того чтобы игроку пришлось двигаться, а не стоять на месте.

И наконец в функции Eat(), эта функция которая вызывается в отдельном потоке от игрового цикла. Она проверяет не слишком ли много еды на карте, если еды больше 10 ед. то НЕ вызывает функцию SpawnEat(), если меньше 10 ед. то вызывает SpawnEat().

Вот какой она получилась:

Eat()

def Eat(self):
    while True:
        sleep(4)
        if len([i for i in self.OBJECTS if i["name"] == "meat"]) < 10:
            self.SpawnEat()
        sleep(1)
        self.MinimizeHungry()

Функция Start(), для запуска основного цикла:

Start()

def Start(self):
    while True:  
        self.CheckAll()
        self.DrawAll()
        sleep(0.01)

И функция run(), которая запускает всю игру.

run()

def run(self):
    proc1 = threading.Thread(target=self.Start)
    proc1.start()
    proc2 = threading.Thread(target=self.Eat)
    proc2.start()

Сам процесс поедания, я реализовал просто в функции CheckAll() и CheckKeysObjects(). В CheckKeysObjects() я проверял не нажал ли игрок на кнопку E. Если нажал, то ставил переменную self.PRESS_E в True.

В цикле CheckAll(), проверял, не еда ли нынешний объект в цикле for, если еда то проверял не сталкивается ли с ним игрок, если сталкивается то проверял переменную self.PRESS_E, и если она в True то тогда просто удалял объект, и увеличивал голод, Т.Е. переменную self.PLAYER["hungry"].

Вот так это в коде

for object_now in range(len(self.OBJECTS)):
    ....
    if self.OBJECTS[object_now]["name"] == "meat":
        items_objects.append(object_now)
        is_clash = self.IsClash(
            x=self.OBJECTS[object_now]["x"],
            y=self.OBJECTS[object_now]["y"],
            h=self.OBJECTS[object_now]["h"],
            w=self.OBJECTS[object_now]["w"],
            x2=self.PLAYER["x"],
            y2=self.PLAYER["y"],
            h2=self.PLAYER["h"],
            w2=self.PLAYER["w"],
        )

        if is_clash:
            if self.PRESS_E:
                try:
                    self.PLAYER["hungry"] += self.HUNGRUY_ADD
                    del self.OBJECTS[object_now]
                    break

                except IndexError:
                    pass

Скажу наперед, это все мне нужно будет переписовать, когда я буду делать инветнарь

Делаю инвентарь

Итак, настало сложное, нам нужно сделать инвентарь.

Сложность в том что все предметы нужно будет отображать, хранить историю, удалять, ставить на пол объекты.

Я начал с того что добавил игроку новый ключ, это был self.PLAYER["inventory"], там хранятся 4 яцчейки, вот в таком виде:

"inventory":{
    "0":{"status":"space","name":"#0", "minimize_image":"#0"},
    "1":{"status":"space","name":"#1", "minimize_image":"#1"},
    "2":{"status":"space","name":"#2", "minimize_image":"#2"},
    "3":{"status":"space","name":"#3", "minimize_image":"#3"},
}

цифры — просто номера ячеек.

status — этот ключ хранит в себе значение, пуста яйчейка или нет. Если пуста то «space», если же там есть предмет, то там хранится имя предмета.

name — хранит в себе имя предмета, оно будет использовано когда игрок будет класть предмет.

minimize_image — эта уменьшенная картинка предмета которая изображается в инвентаре игрока.

После, сделал новые проверки в нашем CheckKeysObjects(), при нажатии на X предмет будет бросаться на землю, и также при нажатии на кнопку E будет вызываться функция self.UseEat(), которую мы сейчас будем разбирать.

Итак, функция self.UseEat() представляет из себя проход по всем ячейкам инвентаря, в поисках еды, и если еда найдена, то она удаляется из инвентаря, и к голоду добавляется 10 единиц. Для удаление предмета из инвентаря я сделал функцию self.DestroyItem(), в которую подается индекс ячейки, и вся ячейкой просто становится по «дефолту» пустой и без ничего.

self.DestroyItem()

def DestroyItem(self,index_item: str):
    item = self.PLAYER["inventory"][index_item]
    self.PLAYER["inventory"][index_item] = self.PLAYER["default_inventory_item"](index_item)
    self.PLAYER["inventory_must_update"] = True
    return item

self.CheckKeysObjects()

def CheckKeysObjects(self):
    self.WALK_LEFT_PLAYER = False
    self.WALK_RIGHT_PLAYER = False
    self.WALK_UP_PLAYER = False
    self.WALK_DOWN_PLAYER = False
    if key("a"):
        self.WALK_LEFT_PLAYER = True
    elif key("d"):
        self.WALK_RIGHT_PLAYER = True
    if key("w"):
        self.WALK_UP_PLAYER = True
    elif key("s"):
        self.WALK_DOWN_PLAYER = True
    if key("f"):
        self.KEY_F = True
    else:
        self.KEY_F= False
    if key("e"):
        self.UseEat()

self.UseEat()

def UseEat(self):
    for inventory_item in range(len(self.PLAYER["inventory"])):
        if self.PLAYER["inventory"][str(inventory_item)]["name"] == "meat":
            if self.PLAYER["hungry"] + self.ADD_HUNGRY_COUNT < 100.0:
                self.PLAYER["hungry"] += self.ADD_HUNGRY_COUNT
                self.DestroyItem(index_item=str(inventory_item))

Дальше функция бросания предмета на землю.

Там впрочем ничего сложного, при нажатии на X вызывается функция self.QuitItem(), в ней проходит цикл for по всем ячейкам инвентаря, и если ключ ["status"] не ровняется "space", то эту ячейку удаляем с помощью ранее рассмотренной функции self.DestroyItem(), и создаем объект на основе того что был в ячейке, X и Y ставит игрока, как бы он бросил его возле себя.

self.Quititem()

def QuitItem(self):
    for inventory_item in range(len(self.PLAYER["inventory"])):
        if self.PLAYER["inventory"][str(inventory_item)]["status"] != "space":
            self.CreateObject(
                img=self.PLAYER["inventory"][str(inventory_item)]["img"],
                x=self.PLAYER["x"],
                y=self.PLAYER["y"],
                name=self.PLAYER["inventory"][str(inventory_item)]["name"],
                data=self.PLAYER["inventory"][str(inventory_item)]["data"],
            )
            self.DestroyItem(index_item=str(inventory_item))
            break

И впрочем все, многие вещи я не говорил как сделал, Т.К. они являлись не основной частью игры, хоть и интересной. Например сообщения о возможности поднятие предмета или нет (когда инвентарь полон), о том что я добавил анимацию ходьбы, о том что сделал отдельную библиотеку картинок, и тому прочих вещах.

На этом все?

Нет, я собираюсь в игру добавить нейросеть, используя библиотеку который я писал на Python,
Собираюсь сделать взаимодействие игрока с NPC оснащенными нейросетью,
небольшой, но какой-нибудь сюжет, и также какие-то припасы для игрока типа брони, еды. предметов, возможность строить блоками.

Попробовать игру

Ее можно свободно скачать с моего GitHub, для запуска потребуется лишь Python3, и библиотека keyboard. Запускать нужно файл main.py.

Игра


Загрузить PDF


Загрузить PDF

Из этой статьи вы узнаете, как создать простую текстовую игру в командной строке на компьютере с Windows.

  1. Изображение с названием 538705 1

    1

    Откройте Блокнот. Блокнот — это бесплатный текстовый редактор, включаемый во все версии Windows. Используйте Блокнот для ввода программного кода. Чтобы открыть Блокнот:

    • Откройте меню «Пуск»
      Windows Start

      .

    • Введите блокнот.
    • Нажмите «Блокнот» в верхней части меню.
  2. Изображение с названием 538705 2

    2

    Введите название игры. Скопируйте следующий текст в Блокнот — вместо [Название] введите название игры, а затем нажмите Enter:[1]

    @echo off
    title [Название]
    
  3. Изображение с названием 538705 3

    3

    Определитесь с цветом текста и фона игры. Чтобы указать цвет, нужно ввести код в формате «0A», где «0» — это цвет фона, а «A» — это цвет текста. Коды распространенных цветов:[2]

    • Цвет текста — используйте буквы A, B, C, D, E или F для обозначения светло-зеленого, светло-синего, светло-красного, светло-фиолетового, светло-желтого или белого цвета, соответственно.
    • Цвет фона — используйте цифры 0, 1, 2, 3, 4, 5, 6, 7, 8 или 9 для обозначения черного, синего, зеленого, морской волны, красного, фиолетового, желтого, белого, серого или голубого цвета, соответственно.
    • Например, чтобы создать белый текст на черном фоне, введите код «0F».
  4. Изображение с названием 538705 4

    4

    Задайте цвета игры. Скопируйте следующий текст в Блокнот — вместо «0A» введите нужную комбинацию цветов фона и текста, а затем нажмите Enter:

    @echo off
    title Онлайн-игра
    color 0A
    if "%1" neq "" ( goto %1)
    
  5. Изображение с названием 538705 5

    5

    Создайте игровое меню. Оно отобразится после запуска игры. Скопируйте следующий текст в Блокнот, а затем нажмите Enter:

    :Menu
    cls
    echo 1. Пуск
    echo 2. Благодарность
    echo 3. Выход
    set /p answer=Нажмите на клавишу с соответствующей цифрой, а затем нажмите Enter : 
    if %answer%==1 goto Start_1
    if %answer%==2 goto Credits
    if %answer%==3 goto Exit
    
  6. Изображение с названием 538705 6

    6

    Задайте опцию «Выход». Так игроки смогут выйти из командной строки. Скопируйте следующий текст в Блокнот, а затем нажмите Enter:

    :Exit
    cls
    echo Спасибо за игру!
    pause
    exit /b
    
  7. Изображение с названием 538705 7

    7

    Задайте опцию «Благодарность». Скопируйте следующий текст в Блокнот (вместо [Название] введите название игры), а затем нажмите Enter:

    :Credits
    cls
    echo Благодарность
    echo.
    echo Спасибо за то, что играли в [Название]!
    pause
    goto Menu
    
  8. Изображение с названием 538705 8

    8

    Задайте опцию «Пуск». Она позволит игрокам начать новую игру:

    :Start_1
    cls
    echo О нет! Вы окружены врагами.
    echo Их пятеро, и все они вооружены.
    echo Если вы сразитесь с ними, у вас есть шансы на победу.
    set /p answer=Сразиться или убежать?
    if %answer%==сразиться goto Fight_1
    if %answer%==убежать goto Run_1
    pause
    
  9. Изображение с названием 538705 9

    9

    Введите код действия. Введете следующий код, чтобы задать действие игры:

    :Run_1
    cls
    echo Вы выжили, чтобы сразиться в другой раз.
    pause
    goto Start_1
    :Fight_1
    echo Приготовьтесь к схватке.
    echo Враги внезапно атаковали вас.
    set /p answer= Нажмите 1, а затем нажмите Enter.
    if %answer%==1 goto Fight_1_Loop
    :Fight_1_Loop
    set /a num=%random%
    if %num% gtr 4 goto Fight_1_Loop
    if %num% lss 1 goto Fight_1_Loop
    if %num%==1 goto Lose_Fight_1
    if %num%==2 goto Win_Fight_1
    if %num%==3 goto Win_Fight_1
    if %num%==4 goto Win_Fight_1
    :Lose_Fight_1
    cls
    echo Вы проиграли. Сыграть еще раз?
    pause
    goto Menu
    :Win_Fight_1
    cls
    echo Вы победили!
    set /p answer=Хотите сохранить игру? [д/н]
    if %answer%=='д' goto 'Save'
    if %answer%=='н' goto 'Start_2'
    :Save
    goto Start_2
    
  10. Изображение с названием 538705 10

    10

    Откройте меню Файл. Оно находится в верхнем левом углу Блокнота.

  11. Изображение с названием 538705 11

    11

    Нажмите Сохранить как. Эта опция находится в меню «Файл». Откроется окно «Сохранить как».

  12. Изображение с названием 538705 12

    12

    Введите имя файла, а затем введите расширение «.bat». В текстовом поле «Имя файла» в нижней части окна введите любое имя файла, а затем введите расширение .bat, чтобы игра сохранилась в виде пакетного файла.

    • Например, чтобы назвать игру «Dungeon Crawl», введите Dungeon Crawl.bat.
  13. Изображение с названием 538705 13

    13

    Измените формат файла. Откройте меню «Тип файла» в нижней части окна, а затем выберите в нем «Все файлы».

  14. Изображение с названием 538705 14

    14

    Сохраните игру на рабочем столе. Для этого нажмите «Рабочий стол» на левой панели. Возможно, сначала вам придется прокрутить панель вверх или вниз, чтобы найти папку «Рабочий стол».

  15. Изображение с названием 538705 15

    15

    Нажмите Сохранить. Это кнопка в правом нижнем углу окна. Игра будет сохранена как BAT-файл.

  16. Изображение с названием 538705 16

    16

    Запустите игру. Дважды щелкните по BAT-файлу, чтобы открыть игру в командной строке, а затем следуйте инструкциям на экране.

    • Например, нажмите «1», чтобы начать игру.
  17. Изображение с названием 538705 17

    17

    Измените программный код игры. Теперь, когда вы написали основной код, в него можно внести изменения, например, добавить параметры и многое другое.

    • Чтобы отредактировать программный код игры, щелкните правой кнопкой мыши по BAT-файлу, а затем в меню выберите «Изменить». Внесите изменения и нажмите Ctrl+S, чтобы сохранить их.
    • Обязательно просмотрите код, чтобы выяснить, за что отвечает каждая его строка.

    Реклама

Советы

  • Если вы хотите использовать более продвинутый редактор кода, скачайте и установите Notepad ++, а затем создайте в нем файл BAT.
  • Чтобы вывести текст на экран, перед этим текстом введите команду echo.
  • Играйте в игру, даже если она не совсем закончена; так вы выясните, какой текст отображается на экране, и сможете найти ошибки или опечатки.
  • Пакетные BAT-файлы используются в Windows для автоматизации задач, но написание такой текстовой игры — это способ узнать, как они работают.

Реклама

Предупреждения

  • Проверьте программный код, перед тем как передавать BAT-файл другим людям.
  • Не скачивайте и не запускайте BAT-файлы, полученные из сомнительных источников, так как они могут содержать вирусы.

Реклама

Об этой статье

Эту страницу просматривали 45 205 раз.

Была ли эта статья полезной?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
@echo off
REM Author: nsinreal
REM Blog: [url]http://nsinreal.blogspot.com[/url]
REM E-Mail: [email]nsinreal+batch@gmail.com[/email]
REM XMPP: xmpp://nsinreal@jabber.ru
REM License: cc-by-sa 3.0
 
REM BETA-TEST!!!
REM Update №3
goto :init
 
:init
REM А мы такие замкнутые... cmd.exe -> setlocal /?
setlocal>nul
REM Меняем цвет. Честно, это не нужно, но оставил для приличия.
color 07 >nul
REM Устанавливаем заголовок
title Minesweeper by nsinreal
REM Очищаем экран. Да, в конце игры вам будет предложено сыграть заново. Ну не оставлять же это безобразие?
cls
REM Создаем поле. Максимальное кол-во бомб (maxbombs) - 20. Устанавливаем кол-во бомб в 0, кол-во проставленных флагов в 0.
call :logo
echo Generation field. Please, wait...
set maxbombs=17
set bombs=0
set flags=0
REM Не собираемся ни выходить, ни умирать не выиграывать в начале игры. Хотя, мы даже игру не стартанули
set die=0
set win=0
set quit=0
set started=0
set step=0
REM Собсно сам процес создания полей. mfield - поле, выводимое на экран; rfield - реальное поле
for /L %%x in (1,1,9) do for /L %%y in (1,1,9) do set mfield%%x%%y=?
for /L %%x in (1,1,9) do for /L %%y in (1,1,9) do set rfield%%x%%y=?
call :genbomb
call :genrfield
REM Переходим к игре.
goto :gamecycle
REM goto:eof - это что-то типа return или end в функции/процедуре.
goto:eof
 
:gamecycle
REM Так как это... батник глючит из-за особенностей cmd.exe и нежелания читать разработчиков (как и MS, так и меня) доку, то делаем финт ушами:
REM Если переменная quit не равна 0, то выходим из игры.
if not "%quit%"=="0" (
    endlocal>nul
    goto:eof
    )
REM Очистка экрана.
cls
REM Вывод поля и дополнительной информации.
set line0=:yx  1  2  3  4  5  6  7  8  9  yx:
for /L %%y in (1,1,9) do call set line%%y=:%%y:  %%mfield%%y1%%  %%mfield%%y2%%  %%mfield%%y3%%  %%mfield%%y4%%  %%mfield%%y5%%  %%mfield%%y6%%  %%mfield%%y7%%  %%mfield%%y8%%  %%mfield%%y9%%  :%%y:
REM Изменяем внешний вид: все нули меняем на ноль, а все воспросительные знаки (?) на точки (.).
for /L %%y in (1,1,9) do call set line%%y=%%line%%y:0= %%
for /L %%y in (1,1,9) do call set line%%y=%%line%%y:?=.%%
if %flags% GTR %maxbombs% (
    set line1=%line1%  !!! F
    ) else (
    set line1=%line1%      F
    )
REM Печатаем кол-во флагов.
set line1=%line1%lags: %flags%
set line3=%line3%      Step: %step%
REM Выписываем справку
if not "%die%"=="1" if not "%win%"=="1" (
    set line4=%line4%      Avaibled commands:
    set line5=%line5%      h      - Output this help.
    set line6=%line6%      o [yx] - Open point yx
    set line7=%line7%      f [yx] - Create flag on point yx
    set line8=%line8%      n      - Start new game
    set line9=%line9%      q      - Exit to Windows or console
    )
REM Считаем время. Странно, но если вызывать :checkstart отсюда, а не как правильно, то все работает правильно. Очень странно.
call :checkstart
if not "%started%"=="0" (
    call :endtime
    set line2=%line2%      Time: %gametime%
    ) else (
    set line2=%line2%  !!! Game not started yet.
    )
if "%die%"=="1" set line4=%line4%      BOMB!!! U died.
if "%win%"=="1" set line4=%line4%      WIN!!! WIN!!! WIN!!! WIN!!! WIN!!!
echo %line0%      Bombs: %bombs%
echo :---------------------------------:
for /L %%y in (1,1,8) do call :echofield line%%y
echo %line9%
echo :---------------------------------:
echo %line0%
echo.
REM Проверяем, выиграл ли пользователь.
if not "%die%"=="1" if not "%win%"=="1" call :wincheck
REM Некоторые идиотские проверочки: если мы умерли или выиграли, то выводим запрос на создание новой игры.
if "%die%"=="1" (
    goto:bombdie
    ) else if "%win%"=="1" (
        goto:dowin 
        ) else (
            REM Считываем и обрабатываем данные с клавиатуры.
            goto inputcycle
        )
goto:eof
 
:inputcycle
REM См. первый комент в :gamecycle.
if not "%quit%"=="0" (
    endlocal>nul
    goto:eof
    )
REM Заносим в input магическую строку. Так как переменная input может и не создатся (пользователь нажал Enter), то вводим в неё изначально такой бред. 
REM Так легче, чем использовать IF DEFINED, который, кстати, не всегда работает.
set input=0 00
set /p "input=Input: "
set input=%input: =%
REM Первая буква - необходимое действие.
set action=%input:~0,1%
REM Уходим отсюда.
if "%action%"=="q" (
    set quit=1
    echo Good Bye.
    goto:eof
    )
REM Выводим справку.
if "%action%"=="h" (
    cls
    call :help
    goto:gamecycle
    )
REM Выводим список выигранных игр.
if "%action%"=="r" if EXIST records.log (
    cls
    type records.log
    pause>nul
    goto:gamecycle
    ) else (
    call:errorIO2
    )
if "%action%"=="n" (
    endlocal>nul
    goto init
    )
REM См. первый комент в :gamecycle.
if not "%quit%"=="0" (
    endlocal>nul
    goto:eof
    )
REM Издеваемся над координатами.
REM Честно, мне лень переписывать.
set ix=0
set iy=0
for /L %%a in (1,1,9) do if "%%a"=="%input:~0,1%" set ix=%%a
if %ix%==0 (
    for /L %%a in (1,1,9) do if "%%a"=="%input:~1,1%" set ix=%%a
    for /L %%a in (1,1,9) do if "%%a"=="%input:~2,1%" set iy=%%a
    ) else (
    for /L %%a in (1,1,9) do if "%%a"=="%input:~1,1%" set iy=%%a
    REM Координаты можно ввести без "o" в начале
    set action=o
    )
REM Выводим сообщение, что пользователь - дурак.
if not "%action%"=="q" if not "%action%"=="h" if not "%action%"=="o" if not "%action%"=="f" if not "%action%"=="n" if not "%action%"=="r" call:errorIO2
REM Выводим еще одно сообщение, что пользователь - дурак.
if "%ix%"=="0" (
    call :errorIO1
    goto:gamecycle
    )
if "%iy%"=="0" (
    call :errorIO1
    goto:gamecycle
    )
REM Открываем точки, ставим флаги.
call :checkstart
if "%action%"=="o" (
        call :openpoint %ix% %iy% %%rfield%ix%%iy%%% %%mfield%ix%%iy%%%
        goto:gamecycle
    )
if "%action%"=="f" (
        call :flagpoint %ix% %iy% %%mfield%ix%%iy%%%
        goto:gamecycle
    )
goto:eof
 
REM Вспомогательные процедуры.
REM Выводим наш опознавательный знак.
:logo
echo                                  ~
echo                                  :77~ ~=~I~
echo                          ~       :? I~?:=7=      ~?~
echo                           I7=~ ~I~+7+?:~?7++?=~=?7=
echo                           :=7 7II77III??,+==~~~=I=~
echo                           ~:=I?~7?+7II??++~=?+~:~~
echo                            I777:=I7I??I??++??=~:::~
echo                           I777 :~+I7III??++?+~~~::~=
echo                      ~?IIII?777   777II??++=~~~~::=777II=
echo                       ~~=?I?77777777II???+?==~~:::~7I+~~
echo                         ~,+?IIII7IIII???++??+~~:::~~:
echo                         ~???~??IIII???++~::,~::::~=++~
echo                       ~~~+~I   ??:~+??++=~,:::::~~=?+++~
echo                      ~:,,I===~~++,=?+====~~~:::~+=+I::::~
echo                           =+=======:::=~~~~:::I  +??
echo                            ==~=======~~~~::::~:~+ ?
echo                             ~=====~~~~~::::~~=,~?=:~
echo                            :+?==~~~~:+  +~~==+I+~==~         MINESWEEPER
echo                            =+~:~~==~,~ 7~=+?I?  ~,~=            BY NSINREAL
echo                           =:      ~:=: ++?~~:       ~
echo                                    ,  7  , ~~
echo                                    ~      ~~
echo.
goto:eof
 
REM Пользователь дурак.
:errorIO1
cls
echo Error I/O: Unknown coordinates
call:help
goto:gamecycle
REM Пользователь дважды дурак.
:errorIO2
cls
echo Error I/O: Unknown command
call:help
goto:gamecycle
REM Выписываем пользователю справку.
:help
echo Avaibled commands:
echo   h      - Output this help.
echo   o [yx] - Open point yx
echo   f [yx] - Create flag on point yx
echo   n      - Start new game
IF EXIST records.log echo   r      - Output all records
echo   q      - Exit to Windows or console
echo.
echo Example: o 12     - open point with coordinates y=1, x=2
echo          f 34     - create flag on point y=3, x=4
echo          n        - start new game
echo          new game - start new game
echo.
pause>nul
goto:eof
 
REM Генерируем бомбы.
:genbomb
set nx=%1
set ny=%2
set r1=%random:~-2%
set r2=%random:~-2%
set r1=%r1:0= %
set r2=%r2:0= %
set r1=%r1: =%
set r2=%r2: =%
set r1=%r1:~0,1%
set r2=%r2:~0,1%
if "%r1%"==0 goto :genbomb %nx% %ny%
if "%r2%"==0 goto :genbomb %nx% %ny%
if "%r1%"=="%nx%" goto :genbomb %nx% %ny%
if "%r2%"=="%ny%" goto :genbomb %nx% %ny%
REM Фокус с call - это преобразование имени переменной в её значение. В принципе, можно сделать "call echo %%rfield%%x%%y%%" и будет выведено содержание текущей клетки.
REM Но вот if там нельзя использовать, поэтому мы делаем вызов процедуры с передачей значения переменной и её имени.
call :newbomb %%rfield%r1%%r2%%% rfield%r1%%r2%
REM Делаем бомбы пока их количество не будет равно максимальному.
if "%bombs%" == "%maxbombs%" goto:eof
goto genbomb
 
REM Создаем бомбы, если её нету. Вызывается из :genbomb.
:newbomb
if not "%1"=="X" (
    set %2=X
    set /a bombs=%bombs%+1
    )
goto:eof
 
REM В сапере обычно используются числа для обозначения кол-ва стоящих рядом бомб. Перебираем все клетки и вносим туда необходимые числа. Опять же, используем фокус с call
:genrfield
for /L %%x in (1,1,9) do for /L %%y in (1,1,9) do call :dosumfield %%x %%y %%rfield%%x%%y%% rfield%%x%%y
goto:eof
 
REM А эта процедура записывает необходимые числа в необходимые клеточки.
:dosumfield
REM %1, %2 - координаты (x, y), %3 - содержание клеточки, %4 - имя переменной.
REM Если клеточка заполнена, то выходим из процедуры.
if not "%3"=="?" goto:eof
REM Устанавливаем координаты ближайших клеточек.
set /a x1=%1 - 1 
set /a y1=%2 + 1
 
set /a x2=%1 
set /a y2=%2 + 1 
 
set /a x3=%1 + 1 
set /a y3=%2 + 1
 
set /a x4=%1 - 1 
set /a y4=%2 
 
set /a x5=%1 + 1 
set /a y5=%2
 
set /a x6=%1 - 1 
set /a y6=%2 - 1
 
set /a x7=%1 
set /a y7=%2 - 1 
 
set /a x8=%1 + 1 
set /a y8=%2 - 1
 
REM sum - количество бомб рядом, обнуляем.
set sum=0
REM Проверяем координаты: GTR больше, LSS меньше. "Массив" у нас задан координатами [1..9,1..9], а в результате предыдущих действий координаты выходили за пределы массива.
if %1 GTR 1 if %2 LSS 9 call :newsum %%rfield%x1%%y1%%%
            if %2 LSS 9 call :newsum %%rfield%x2%%y2%%% 
if %1 LSS 9 if %2 LSS 9 call :newsum %%rfield%x3%%y3%%%
if %1 GTR 1             call :newsum %%rfield%x4%%y4%%%
if %1 LSS 9             call :newsum %%rfield%x5%%y5%%%
if %1 GTR 1 if %2 GTR 1 call :newsum %%rfield%x6%%y6%%%
            if %2 GTR 1 call :newsum %%rfield%x7%%y7%%%
if %1 LSS 9 if %2 GTR 1 call :newsum %%rfield%x8%%y8%%%
set %4=%sum%
goto:eof
 
REM Так как call не воспринимает if, то используем отдельную процедуру.
:newsum
if "%1"=="X" set /a sum+=1
goto:eof
 
REM Удаляем числа
:delrfield
for /L %%x in (1,1,9) do for /L %%y in (1,1,9) do call :delsumfield %%x %%y %%rfield%%x%%y%% rfield%%x%%y
goto:eof
 
:delsumfield
if "%3"=="?" goto:eof
if "%3"=="X" goto:eof
set %4=?
goto:eof
 
 
REM Эта процедура относится к выводу поля. Я не знаю как еще в for-цикле использовать две и более команды, кроме как использовать отдельную процедуру. 
:echofield
REM Выводим поле. И пустую строчку.
call echo %%%1%%
echo :-:                             :-:
goto:eof
 
REM Открываем клеточку
:openpoint
rem %1, %2 - x, y; %3 - значение клетки реального поля; %4 - значение клетки видимого поля.
set /a step+=1
REM Если клетка не пуста - рассказываем пользователю много интересного, если в клетке бомба - уже поздно что-либо рассказывать.
if not "%4"=="?" (
    echo Point x=%1 y=%2 already opened
    pause>nul
    goto:eof
    )
if "%3"=="X" if not "%step%"=="1" (
    set die=1
    for /L %%x in (1,1,9) do for /L %%y in (1,1,9) do call set mfield%%x%%y=%%rfield%%x%%y%%
    goto:eof
    )
if "%step%"=="1" if "%3"=="X" (
    set rfield%1%2=?
    set /a bombs-=1
    call :delrfield
    call :genbomb %1 %2
    call :genrfield
    rem call set mfield%1%2=%%rfield%1%2%%
    call :oaf %1 %2 %%rfield%1%2%% %%mfield%1%2%%
    )
REM А если ни то, ни другое, то пытаемся открыть эту и ближние клетки.
call :oaf %1 %2 %3 %4
goto:eof 
 
:oaf
rem %1, %2 - x, y; %3 - значение клетки реального поля; %4 - значение клетки видимого поля.
REM Если клетка пуста (выход за пределы поля) или в ней бомба - уходим отсюда.
if "%3"=="" goto:eof
if "%3"=="X" goto:eof
 
REM Открываем данную клетку
call set mfield%1%2=%%rfield%1%2%%
 
REM Если в данной клетке 0 бомб, то пытаемся открыть все ближние клетки.
if not "%3" == "0" goto:eof
 
REM xn, yn - координаты следующей клетки. Диагональ не проверяется.
set /a xn=%1
set /a yn=%2 + 1
REM Проверка, открыта ли эта клетка на видимом поле.
set dooaf=0
call :checkoaf %%mfield%xn%%yn%%%
REM Если нет - пытаемся открыть.
if %dooaf%==1 call :oaf %xn% %yn% %%rfield%xn%%yn%%% %%mfield%xn%%yn%%%
 
set /a xn=%1
set /a yn=%2 - 1
set dooaf=0
call :checkoaf %%mfield%xn%%yn%%%
if %dooaf%==1 call :oaf %xn% %yn% %%rfield%xn%%yn%%% %%mfield%xn%%yn%%%
 
 
set /a xn=%1 - 1
set /a yn=%2
set dooaf=0
call :checkoaf %%mfield%xn%%yn%%%
if %dooaf%==1 call :oaf %xn% %yn% %%rfield%xn%%yn%%% %%mfield%xn%%yn%%%
 
set /a xn=%1 + 1
set /a yn=%2
set dooaf=0
call :checkoaf %%mfield%xn%%yn%%%
if %dooaf%==1 call :oaf %xn% %yn%   %%rfield%xn%%yn%%% %%mfield%xn%%yn%%%
 
REM Диагональ.
set /a xn=%1 + 1
set /a yn=%2 + 1
set dooaf=0
call :checkoaf %%mfield%xn%%yn%%% %%rfield%xn%%yn%%%
if %dooaf%==1 call :oaf %xn% %yn%   %%rfield%xn%%yn%%% %%mfield%xn%%yn%%%
 
set /a xn=%1 + 1
set /a yn=%2 - 1
set dooaf=0
call :checkoaf %%mfield%xn%%yn%%% %%rfield%xn%%yn%%%
if %dooaf%==1 call :oaf %xn% %yn%   %%rfield%xn%%yn%%% %%mfield%xn%%yn%%%
 
set /a xn=%1 - 1
set /a yn=%2 + 1
set dooaf=0
call :checkoaf %%mfield%xn%%yn%%% %%rfield%xn%%yn%%%
if %dooaf%==1 call :oaf %xn% %yn%   %%rfield%xn%%yn%%% %%mfield%xn%%yn%%%
 
set /a xn=%1 - 1
set /a yn=%2 - 1
set dooaf=0
call :checkoaf %%mfield%xn%%yn%%% %%rfield%xn%%yn%%%
if %dooaf%==1 call :oaf %xn% %yn%   %%rfield%xn%%yn%%% %%mfield%xn%%yn%%%
 
goto:eof
 
REM Проверка, открыта ли клетка на видимом поле.
:checkoaf
if "%1"=="?" set dooaf=1
goto:eof
 
REM Устанавливаем флаг
:flagpoint
REM %1, %2 - x, y; %3 - значение клетки видимого поля
REM Если в клетке уже что-то есть - рассказываем пользователю что-то интересное
if not "%3"=="?" if not "%3"=="!" (
    echo Point %2 %1 already opened
    pause>nul
    goto:eof
    )
REM Если нету флага - ставим, если есть флаг - убираем. Так же изменяем счетчик.
if not "%3"=="!" (
    set /a flags+=1
    call set mfield%%1%%2=^!
    ) else (
    set /a flags-=1
    call set mfield%%1%%2=?
    )
goto:eof 
 
REM Бабах... Мы все умрем... А вот пользователь уже подорвался на какой-то бомбе. Неудачливый из него сапер.
:bombdie
REM :bombdie требует вывода поля (заново), поэтому используется дополнительная переменная для обозначения первого запуска :bombdie и одновременно смерти игры.
if not "%die%"=="1" (
    set die=1
    goto :gamecycle
    )
REM Считываем, хочет ли пользователь еще раз сыграть игру.
pause>nul
echo Want to start new game? Example: yes
set sgame=yes
set /p sgame=
set sgame=%sgame:~0,1%
if "%sgame%"=="y" (
    endlocal>nul
    goto :init
    )
if "%sgame%"=="Y" (
    endlocal>nul
    goto :init
    )
set quit=1
goto:eof
 
REM Проверяем, выиграл ли пользователь.
:wincheck
REM Если флагов больше кол-ва бомб, то пользователь ошибся!
if %flags% GTR %maxbombs% goto:eof
REM Если пользователь решил схитрить... то выходим.
if %step%==0 goto:eof
REM Считаем кол-во правильно поставленных флагов и не открытых клеточек.
set nopoints=0
set rflags=0
for /L %%x in (1,1,9) do for /L %%y in (1,1,9) do call :checkfo %%mfield%%x%%y%% %%rfield%%x%%y%%
REM Если количество правильно поставленных флагов равно кол-ву бомб, то юзер выиграл
if %rflags%==%maxbombs% call :dowin
REM Еще один вариант победы человека над машиной: если все флаги проставлены правильно, но стоят не везде; сумма поставленных флагов и неоткрытых клеточек равна кол-ву бомб.
set /a sumpoints=%nopoints% + %rflags%
if %rflags%==%flags% if %sumpoints%==%maxbombs% call :dowin
goto:eof
 
REM Изменяем счетчики.
:checkfo
if "%1"=="?" set /a nopoints+=1
if "%1"=="!" if "%2"=="X" set /a rflags+=1
goto:eof
 
REM Мы выиграли!
:dowin
if not "%win%"=="1" (
    set win=1
    for /L %%x in (1,1,9) do for /L %%y in (1,1,9) do call set mfield%%x%%y=%%rfield%%x%%y%%
    goto :gamecycle
    )
pause>nul
set name=%USERNAME%
set /p name="Your name: "
set name=%name%:%USERNAME%@%USERDOMAIN%
set name=%name:|=%
IF not EXIST records.log echo ----- Minesweeper' records ----- >records.log
echo Name: %name% ^| Steps: %step% ^| GameTime: %gametime% ^| Flags: %rflags% ^| Not opened: %nopoints% ^| Date-Time: %date% %time% >>records.log 2>nul 
echo Want to start new game? Example: yes
set sgame=yes
set /p sgame=
set sgame=%sgame:~0,1%
if "%sgame%"=="y" (
    endlocal>nul
    goto :init
    )
if "%sgame%"=="Y" (
    endlocal>nul
    goto :init
    )
set quit=1
goto:eof
 
REM Первая процедура для подсчета времени
:checkstart
if not "%started%"=="0" goto:eof
set stime=%time%
set sdate=%date:~0,2%
set started=1
call :endtime
goto:eof
 
REM Вторая (она же основная) процедура для подсчета времени
:endtime
REM Издеваемся над временем.
set rtime=%time%
set stime=%stime::= %
set stime= %stime:,= %
set stime=%stime: 0= %
set rdate=%date:~0,2%
set rtime=%rtime::= %
set rtime= %rtime:,= %
set rtime=%rtime: 0= %
REM В %stime% и %rtime% прописано четыре числа (разделители - пробелы): часы, минуты, секунды, миллисекунды
set counter=4
for /L %%a in (1,1,%counter%) do set s%counter%=
for /L %%a in (1,1,%counter%) do set r%counter%=
REM Начинаем обрабатывать строки stime и rtime с конца.
:looptime
for /F "tokens=%counter%" %%a in ("%stime%") do set s%counter%=%%a
for /F "tokens=%counter%" %%a in ("%rtime%") do set r%counter%=%%a
set /a counter-=1
If %counter% GTR 0 goto looptime
REM Выражаем время одним числом
set /a s=%s4% + %s3%*100 + %s2%*6000 + %s1%*360000
set /a r=%r4% + %r3%*100 + %r2%*6000 + %r1%*360000
REM Учитываем разницу в днях.
if %rdate% GTR %sdate% set /a r+=8640000*(%rdate%-%sdate%)
REM Считаем разницу во времени.
set /a d=%r% - %s%
set /a d1=%d% / 360000
set /a td2=%d% %% 360000
set /a d2=%td2% / 6000
set /a td3=%td2% %% 6000
set /a d3=%td3% / 100
set /a td4=%td3% %% 100
set /a d4=%td4%
if %d1% LSS 10 set d1=0%d1%
if %d2% LSS 10 set d2=0%d2%
if %d3% LSS 10 set d3=0%d3%
if %d4% LSS 10 set d4=0%d4%
REM Устанавливаем прошедшее время.
set gametime=%d1%:%d2%:%d3%.%d4%
endlocal >nul

Important EDIT: I’ve decided to keep the most up-to-date version of SNAKE.BAT here at the top of this first post. A description of the most recent changes will be placed near the end of the thread. Below the code is the original post. Read the entire thread to get a sense of the evolution of the game..

Code: Select all

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: SNAKE.BAT - A pure native Windows batch implementation of the classic game
:: ------------------------------------------------------------------------------
:: Written by Dave Benham with some debugging help and technique pointers from
:: DosTips users - See http://www.dostips.com/forum/viewtopic.php?f=3&t=4741
::
:: The game should work on any Windows machine from XP onward using only batch
:: and native external commands. However, the default configuration will most
:: likely have some screen flicker due to the CLS command issued upon every 
:: screen refresh. There are two ways to eliminate screen flicker:
::
:: 1 - "Pure batch" via VT100 escape sequences:
:: You can eliminate flicker by enabling the VT100 mode within the game's
:: Graphic options menu. However, this mode requires a console that supports
:: VT100 escape sequences. This comes standard with Windows 10 (and beyond).
:: The Windows 10 console must be configured properly for this to work - the
:: "Legacy Console" option must be OFF. Prior to Windows 10, there was no
:: standard Windows console that supported VT100 escape sequences, though you
:: may find a utility that provides that support.
::
:: 2 - CursorPos.exe cheat from Aacini:
:: You can eliminate screen flicker on any Windows version by placing Aacini's
:: CursorPos.exe in the same folder that contains SNAKE.BAT. This method of
:: eliminating flicker is "cheating" in that it is not pure native batch since
:: it relies on a 3rd party tool. A script to create CursorPos.exe is available
:: at http://goo.gl/hr6Kkn.
::
:: Note that user preferences and high scores are stored in %USERPROFILE%\Snake
:: User saved games have an implicit .snake.txt "extension", and are saved and
:: loaded from the current directory.
::
:: Version History
::
:: 4.1  2018-09-08
::   - Fixed bug in Playfield too large error handling that aborted but hung.
::
:: 4.0  2017-04-10
::   - New Field size options, ranging from tiny to large. Original = Medium.
::   - Reconfigured menu
::   - Added support for VT100 mode to eliminate screen flicker by using
::     with "pure" batch VT100 escape sequences.
::
:: 3.8  2015-02-16
::   - Improve performance of Replay abort
::   - Eliminate flicker at game start when using CursorPos.exe
::   - Use symbols (variables) for lock, key and cmd streams.
::
:: 3.7  2014-08-03
::   - Reduced screen flicker when playing without CursorPos.exe by building
::     entire screen in one variable before CLS and ECHOing the screen.
::
:: 3.6  2014-04-09
::   - Pause after displaying CursorPos.exe message at end if game was launced
::     via double click or START menu.
::
:: 3.5  2014-02-03
::   - Made growth rate user configurable. High scores are now stored for each
::     growth rate played.
::   - Added optional support for Aacini's CursorPos.exe to eliminate screen
::     flicker.
::   - Redesigned storage of configuration options within saved games to make
::     it easier to extend in the future. Existing saved games are automatically
::     converted to the new format.
::   - Simplified replay abort mechanics.
::
:: 3.4  2013-12-26
::   - Added ability to abort a game replay.
::
:: 3.3  2013-12-24
::   - Added Pause functionality.
::
:: 3.2  2013-12-08
::   - Fixed a replay bug. Note that attempting to delete a non-existent file
::     does not raise an error!
::   - Added ability to save a previous game or a High score game to a user
::     named file in the current directory.
::   - Added ability to load and replay a user saved game from the current
::     directory.
::
:: 3.1  2013-12-08
::   - Fixed a bug with the game logs. Must include key mappings in log.
::     High scores from version 3.0 should be deleted from %USERPROFILE%\Snake.
::
:: 3.0  2013-12-07
::   - Made control keys user configurable, including option for 2 key
::     (left/right) or 4 key (left/right/up/down) input.
::   - Made graphics user configurable.
::   - Added ability to display replay of previous game.
::   - Added High Score list, with ability to display replay of High Score games.
::
:: 2.3  2013-12-01
::   - Added elapsed game time to the display.
::
:: 2.2  2013-08-06
::   - Improved comments / internal documentation
::   - A few inconsequential code changes
::
:: 2.1  2013-07-20
::   - Reworked interprocess communication. No more hanging games (I hope).
::   - Fixed parameterization of movement key definition.
::   - Temp file location now controlled by TEMP (or TMP) environment variable.
::   - Implemented a game session lock into temp file names so multiple game
::     instances can share the same TEMP folder without interference.
::
:: 2.0  2013-07-17
::   - First attempt at using XCOPY instead of CHOICE. Game now runs as
::     pure native batch on all Windows versions from XP onward.
::
:: 1.0  2013-07-13  to  1.x
::   - Game required CHOICE command, so did not work on XP without download of
::     a non-standard exe or com file.
::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

@echo off
if "%~1" == "startGame" goto :game
if "%~1" == "startController" goto :controller


::---------------------------------------------------------------------
:: setup some global variables used by both the game and the controller

setlocal disableDelayedExpansion
set "toggleVT100="
:getSession
if defined temp (set "tempFileBase=%temp%\") else if defined tmp set "tempFileBase=%tmp%\"
set "tempFileBase=%tempFileBase%Snake%time::=_%"
set "keyFile=%tempFileBase%_key.txt"
set "cmdFile=%tempFileBase%_cmd.txt"
set "gameLock=%tempFileBase%_gameLock.txt"
set "gameLog=%tempFileBase%_gameLog.txt"
set "signal=%tempFileBase%_signal.txt"
set "saveLoc=%userprofile%\Snake"
set "userPref=%saveLoc%\SnakeUserPref.txt"
set "hiFile=%saveLoc%\Snake!size!!growth!Hi"
set "keyStream=9"
set "cmdStream=8"
set "lockStream=7"


::------------------------------------------
:: Lock this game session and launch.
:: Loop back and try a new session if failure.
:: Cleanup and exit when finished

call :launch %lockStream%>"%gameLock%" || goto :getSession
del "%tempFileBase%*"
exit /b


::------------------------------------------
:launch the game and the controller

call :fixLogs
:relaunch
copy nul "%keyFile%" >nul
copy nul "%cmdFile%" >nul
start "" /b cmd /c ^""%~f0" startController %keyStream%^>^>"%keyFile%" %cmdStream%^<"%cmdFile%" 2^>nul ^>nul^"
cmd /c ^""%~f0" startGame %keyStream%^<"%keyFile%" %cmdStream%^>^>"%cmdFile%" ^<nul^"
echo(


::--------------------------------------------------------------
:: Upon exit, wait for the controller to close before returning

:close
2>nul (>>"%keyFile%" call )||goto :close
if "%=exitcode%" equ "00000002" (
  set "toggleVT100=1"
  goto :relaunch
) else if "%=exitcode%" equ "00000001" (
  echo Game play can be improved by installing
  echo Aacini's CursorPos.exe, available at
  echo http://goo.gl/hr6Kkn
  echo(
  echo Alternatively, if your console supports
  echo VT100 escape sequences, then you can
  echo enable VT100 mode within the SNAKE.BAT
  echo Graphic options menu.
  echo(
  echo %cmdcmdline%|find /i "%~f0">nul&&pause
)
exit /b 0


::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:game
title %~nx0
cls

::---------------------------------------
:: Default playfield size
:: max playing field: (width-2)*(height-2) <= 1365

set "size=Medium"
set "dispWidth=40"   max=99
set "dispHeight=25"  max=99
set "defaultSize=%size%"
set /a "defaultWidth=dispWidth, defaultHeight=dispHeight"

::----------------------------
:: Other default values

set "moveKeys=4"

set "up=W"
set "down=S"
set "left=A"
set "right=D"
set "pause=P"

set "space= "
set "bound=#"
set "food=+"
set "head=@"
set "body=O"
set "death=X"

set "vt0=OFF"
set "vt1=ON"
set "vt=0"

set "growth=1"

::--- sendCmd macro ---
:: sendCmd  command
:::  sends a command to the controller
set "sendCmd=>&%cmdStream% echo"

::---------------------------
:: Load user preferences
if exist "%userPref%" for /f "usebackq delims=" %%V in ("%userPref%") do set "%%V"
call :resize

::---------------------------
:: Variable constants

set "configOptions=diffCode difficulty growth moveKeys up down left right size dispWidth dispHeight"

for %%S in (
 "T Tiny   15 10"
 "S Small  25 15"
 "M Medium 40 25"
 "L Large  47 32"
 "W Wide   82 19"
 "N Narrow 20 40"
) do for /f "tokens=1-4" %%A in (%%S) do (
  set "size%%A=%%B"
  set /a "width%%A=%%C, height%%A=%%D"
)

set "spinner1=-"
set "spinner2=\"
set "spinner3=|"
set "spinner4=/"
set "spinner= spinner1 spinner2 spinner3 spinner4 "

set "delay1=20"
set "delay2=15"
set "delay3=10"
set "delay4=7"
set "delay5=5"
set "delay6=3"

set "desc1=Sluggard"
set "desc2=Crawl"
set "desc3=Slow"
set "desc4=Normal"
set "desc5=Fast"
set "desc6=Insane"

set "spinnerDelay=3"

:: define LF as a Line Feed (newline) character
set ^"LF=^

^" Above empty line is required - do not remove

:: define CR as a Carriage Return character
for /f %%A in ('copy /Z "%~dpf0" nul') do set "CR=%%A"

:: define BS as a BackSpace character
for /f %%A in ('"prompt $H&for %%B in (1) do rem"') do set "BS=%%A"

set "upper=A B C D E F G H I J K L M N O P Q R S T U V W X Y Z"
set "invalid=*~="

::---------------------------
:: define macros

if %vt% equ 1 (
  for /f "delims=" %%E in (
    'forfiles /p "%~dp0." /m "%~nx0" /c "cmd /c echo(0x1B"'
  ) do (
    cls
    <nul set /p "=%%E7"
    set "cls=<nul set /p "=%%E8""
    set "ClearLine=<nul set /p "=%%E[K""
    set "ClearPrev=echo(&echo(%%E[F%%E[K"
    set "Up4=echo(%%E[F%%E[F%%E[F%%E[F%%E[F"
    set "ShowCursor=<nul set /p "=%%E[?25h""
    set "HideCursor=<nul set /p "=%%E[?25l""
    set "exitCode=0"
  )
) else if exist "%~dp0CursorPos.exe" (
  set "cls=CursorPos 0 0"
  set "ClearLine=echo(                                   &CursorPos 0 -1"
  set "ClearPrev=CursorPos 0 -0&echo(                                   "
  set "Up4=CursorPos 0 -4"
  set "ShowCursor="
  set "HideCursor="
  set "exitCode=0"
) else (
  set "cls=cls"
  set "ClearLine="
  set "ClearPrev="
  set "Up4="
  set "ShowCursor="
  set "HideCursor="
  set "exitCode=1"
)

:: define a newline with line continuation
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"

:: setErr
:::  Sets the ERRORLEVEL to 1
set "setErr=(call)"

:: clrErr
:::  Sets the ERRORLEVEL to 0
set "clrErr=(call )"

:: getKey  [ValidKey]  [ValidKey...]
::: Check for keypress from the controller. Only accept a ValidKey.
::: Token delimiters and poison characters must be quoted.
::: Accept any key if no ValidKey specified.
::: Return result in Key variable. Key is undefined if no valid keypress.
set getKey=%\n%
for %%# in (1 2) do if %%#==2 (%\n%
  set key=%\n%
  set inKey=%\n%
  set keyTest=%\n%
  ^<^&%keyStream% set /p "inKey="%\n%
  if defined inKey (%\n%
    set inKey=!inKey:~0,-1!%\n%
    for %%C in (!args!) do set /a keyTest=1^&if /i !inKey! equ %%~C set key=!inKey!%\n%
  )%\n%
  if not defined keyTest set key=!inKey!%\n%
) else set args=


:: draw
:::  draws the board
set draw=%\n%
set screen=%\n%
for /l %%Y in (0,1,!height!) do set screen=!screen!!line%%Y!!LF!%\n%
set screen=!screen!Speed = !Difficulty! !replay!!LF!Growth Rate = !growth!   HighScore = !hi!!LF!Score = !score!   Time = !m!:!s!%\n%
if defined replay if not defined replayFinished (%\n%
  set screen=!screen!!LF!!LF!Press a key to abort the replay%\n%
)%\n%
%cls%^&echo(!screen!

:: test  X  Y  ValueListVar
:::  tests if value at coordinates X,Y is within contents of ValueListVar
set test=%\n%
for %%# in (1 2) do if %%#==2 (for /f "tokens=1-3" %%1 in ("!args!") do (%\n%
  for %%A in ("!line%%2:~%%1,1!") do if "!%%3:%%~A=!" neq "!%%3!" %clrErr% else %setErr%%\n%
)) else set args=


:: plot  X  Y  ValueVar
:::  places contents of ValueVar at coordinates X,Y
set plot=%\n%
for %%# in (1 2) do if %%#==2 (for /f "tokens=1-3" %%1 in ("!args!") do (%\n%
  set "part2=!line%%2:~%%1!"%\n%
  set "line%%2=!line%%2:~0,%%1!!%%3!!part2:~1!"%\n%
)) else set args=


::--------------------------------------
:: start the game
setlocal enableDelayedExpansion
if not exist "%saveLoc%\" md "%saveLoc%"
set "replay= Aborting... "
set "replayAvailable="
if exist "!gameLog!" set "replayAvailable=R"
call :loadHighScores
call :mainMenu


::--------------------------------------
:: main loop (infinite loop)
for /l %%. in () do (

  %=== check for and process abort signal if in replay mode ===%
  if defined replay if exist "%signal%" (
    del "%signal%"
    set "replayFinished=1"
    %draw%
    echo(
    %ClearLine%
    <nul set /p "=Aborting... "
    findstr "^" >nul <&%keyStream%
    for %%A in (!configOptions!) do set "%%A=!%%ASave!"
    %ShowCursor%
    call :mainMenu
  )

  %=== compute time since last move ===%
  for /f "tokens=1-4 delims=:.," %%a in ("!time: =0!") do set /a "t2=(((1%%a*60)+1%%b)*60+1%%c)*100+1%%d-36610100, tDiff=t2-t1"
  if !tDiff! lss 0 set /a tDiff+=24*60*60*100

  if !tDiff! geq !delay! (
    %=== delay has expired, so time for movement ===%
    set /a t1=t2

    %=== compute game time ===%
    if not defined gameStart set "gameStart=!t2!"
    set /a "gameTime=(t2-gameStart)"
    if !gameTime! lss 0 set /a "gameTime+=24*60*60*100"
    set /a "gameTime=(gameTime-pauseTime)/100, m=gameTime/60, s=gameTime%%60"
    if !m! lss 10 set "m=0!m!"
    if !s! lss 10 set "s=0!s!"

    %=== get keypress ===%
    %getKey% !keys!
    if /i !key! equ !pause! (

      %=== pause game ===%
      echo(
      %ShowCursor%
      call :ask "PAUSED - Press a key to continue..."
      %HideCursor%
      %ClearPrev%
      %sendCmd% go
      for /f "tokens=1-4 delims=:.," %%a in ("!time: =0!") do set /a "t2=(((1%%a*60)+1%%b)*60+1%%c)*100+1%%d-36610100, tDiff=t2-t1"
      if !tDiff! lss 0 set /a tDiff+=24*60*60*100
      set /a pauseTime+=tDiff

    ) else (

      %=== establish direction ===%
      if not defined replay (echo(!key!.) >>"!gameLog!"
      for %%K in (!key!) do if !moveKeys! equ 2 (
        set /a "xDiff=xTurn%%K*!yDiff!, yDiff=yTurn%%K*!xDiff!"
      ) else if "!%%KAxis!" neq "!axis!" (
        set /a "xDiff=xDiff%%K, yDiff=yDiff%%K"
        set "axis=!%%KAxis!"
      )

      %=== erase the tail ===%
      set "TX=!snakeX:~-2!"
      set "TY=!snakeY:~-2!"
      set "snakeX=!snakeX:~0,-2!"
      set "snakeY=!snakeY:~0,-2!"
      %plot% !TX! !TY! space

      %=== compute new head location and attempt to move ===%
      set /a "X=PX+xDiff, Y=PY+yDiff"
      set "X= !X!"
      set "Y= !Y!"
      set "X=!X:~-2!"
      set "Y=!Y:~-2!"
      (%test% !X! !Y! playerSpace) && (

        %=== move successful ===%

        %=== remove the new head location from the empty list ===%
        for %%X in ("!X!") do for %%Y in ("!Y!") do set "empty=!empty:#%%~X %%~Y=!"

        %=== eat any food that may be present ===%
        (%test% !X! !Y! food) && (
          %=== initiate growth ===%
          set /a grow+=growth

          %=== locate and draw new food ===%
          if defined replay (
            <&%keyStream% set /p "F="
          ) else (
            set /a "F=(!random!%%(emptyCnt-1))*6+1"
            (echo !F!) >>"!gameLog!"
          )
          for %%F in (!F!) do (%plot% !empty:~%%F,5! food)
        )

        if !grow! gtr 0 (
          %=== restore the tail ===%
          %plot% !TX! !TY! body
          set "snakeX=!snakeX!!TX!"
          set "snakeY=!snakeY!!TY!"
          set /a emptyCnt-=1

          %=== manage score ===%
          set /a "score+=1, grow-=1"
          if not defined replay if !score! gtr !hi! set /a "hi+=1, newHi=1"

        ) else (
          %=== add the former tail position to the empty list ===%
          set "empty=!empty!#!TX! !TY!"
        )

        %=== draw the new head ===%
        if defined snakeX (%plot% !PX! !PY! body)
        %plot% !X! !Y! head

        %=== Add the new head position to the snake strings ===%
        set "snakeX=!X!!snakeX!"
        set "snakeY=!Y!!snakeY!"
        set "PX=!X!"
        set "PY=!Y!"

        %draw%

      ) || (

        %=== failed move - game over ===%
        set "replayFinished=1"
        %plot% !TX! !TY! body
        call :spinner !PX! !PY! death
        %draw%
        if defined newHi (
          echo(
          echo New High Score - Congratulations^^!
          set "hi!diffCode!=!score!"
          copy "!gameLog!" "%hiFile%!diffCode!.txt" >nul
          >>"%hiFile%!diffCode!.txt" echo ::!score!
        )
        echo(
        %ClearLine%
        %ShowCursor%
        call :ask "Press a key to continue..."
        for %%A in (!configOptions!) do set "%%A=!%%ASave!"
        call :mainMenu
      )
    )
  )
)


::-------------------------------------
:getString  Prompt  Var  MaxLen
:: Prompt for a string with max lengh of MaxLen.
:: Valid keys are alpha-numeric, space, underscore, and dash
:: String is terminated by Enter
:: Backspace works to delete previous character
:: Result is returned in Var
set /a "maxLen=%3"
set "%2="
%sendCmd% prompt
<nul set /p "=%~1 "
call :purge
:getStringLoop
(%getKey% !upper! 0 1 2 3 4 5 6 7 8 9 " " _ - {Enter} !BS!)
if defined key (
  if !key! equ {Enter} (
    echo(
    exit /b
  )
  if !key! neq !BS! if !maxLen! gtr 0 (
    set /a maxLen-=1
    <nul set /p "=.!BS!!key!"
    set "%2=!%2!!key!
  )
  if !key! equ !BS! if defined %2 (
    set /a maxLen+=1
    <nul set /p "=!BS! !BS!"
    set "%2=!%2:~0,-1!"
  )
)
if defined inKey %sendCmd% one
goto :getStringLoop


::-------------------------------------
:ask  Prompt  ValidKey [Validkey]...
:: Prompt for a keypress.
:: Wait until a ValidKey is pressed and return result in Key variable.
:: Token delimiters, ), and poison characters must be quoted.
%sendCmd% prompt
<nul set /p "=%~1 "
(set validKeys=%*)
(set validKeys=!validKeys:%1=!)
call :purge
:getResponse
(%getKey% !validKeys!)
if not defined key (
  if defined inKey %sendCmd% one
  goto :getResponse
)
exit /b


:purge
set "inKey="
for /l %%N in (1 1 1000) do (
  set /p "inKey="
  if "!inKey!" equ "{purged}." exit /b
)<&%keyStream%
goto :purge


::-------------------------------------
:spinner  X  Y  ValueVar
set /a d1=-1000000
for /l %%N in (1 1 5) do for %%C in (%spinner%) do (
  call :spinnerDelay
  %plot% %1 %2 %%C
  %draw%
)
call :spinnerDelay
(%plot% %1 %2 %3)
exit /b


::-------------------------------------
:delay  centiSeconds
setlocal
for /f "tokens=1-4 delims=:.," %%a in ("!time: =0!") do set /a "spinnerDelay=%1, d1=(((1%%a*60)+1%%b)*60+1%%c)*100+1%%d-36610100"
:: fall through to :spinnerDelay

::-------------------------------------
:spinnerDelay
for /f "tokens=1-4 delims=:.," %%a in ("!time: =0!") do set /a "d2=(((1%%a*60)+1%%b)*60+1%%c)*100+1%%d-36610100, dDiff=d2-d1"
if %dDiff% lss 0 set /a dDiff+=24*60*60*100
if %dDiff% lss %spinnerDelay% goto :spinnerDelay
set /a d1=d2
exit /b


::-------------------------------------
:mainMenu
if defined toggleVT100 call :graphicOptions
cls
call :resize
set "loadAvailable="
echo SNAKE.BAT v4.0 by Dave Benham
echo(
echo Main Menu:
echo(
echo   N - New game
echo   F - Field size..... !size!
echo   W - groWth rate.... !growth!
echo   C - Control options
echo   G - Graphic options

if defined replayAvailable echo   R - Replay previous game
if defined saveAvailable   echo   S - Save a game
if exist *.snake.txt       echo   L - Load and watch a saved game&set "loadAvailable=L"

echo   Q - Quit
echo(
set "hiAvailable="
for /l %%N in (1 1 6) do if defined hi%%N (
  if not defined hiAvailable (
    echo Replay High Score:
    echo(
  )
  set "desc=!desc%%N!........"
  set "hiAvailable=!hiAvailable! %%N"
  echo   %%N - !desc:~0,8! !hi%%N!
)
if defined hiAvailable echo(
set "keys=N F W C G Q !hiAvailable! !replayAvailable! !saveAvailable! !loadAvailable!"
call :ask ">" !keys!
if /i !key! equ Q (
  %sendCmd% quit
  cls
  exit %exitCode%
) else if /i !key! equ N (
  set "replay="
  set "replayAvailable=R"
  set "saveAvailable=S"
  goto :initialize
) else if /i !key! equ S (
  if defined replayAvailable (
    call :ask "HighScore # or P for Previous:" !hiAvailable! P
  ) else (
    call :ask "HighScore #:" !hiAvailable!
  )
  echo !key!
  if /i !key! equ P (set "src=!gameLog!") else set "src=%hiFile%!key!.txt"
  call :getString "Save file name:" file 20
  copy "!src!" "!file!.snake.txt"
  call :ask "Press a key to continue..."
) else if /i !key! equ L (
  call :getString "Load file name:" file 20
  if exist "!file!.snake.txt" (
    set "replay=!file!.snake.txt"
    goto :initialize
  )
  echo Error: File "!file!.snake.txt" not found
  call :ask "Press a key to continue..."
) else if /i !key! equ R (
  set "replay=!gameLog!"
  goto :initialize
) else if /i !key! equ C (
  call :controlOptions
) else if /i !key! equ G (
  call :graphicOptions
) else if /i !key! equ F (
  call :sizeOptions
) else if /i !key! equ W (
  call :ask "Press a digit for growth rate (0 = 10)" 0 1 2 3 4 5 6 7 8 9
  if !key! equ 0 set "key=10"
  set "growth=!key!"
  call :loadHighScores
) else if !key! geq 1 if !key! leq 6 (
  set "replay=%hiFile%!key!.txt"
  goto :initialize
)
goto :mainMenu


::-------------------------------------
:sizeOptions
cls
set "keys=T S M L W N"
echo Field Size Options:
echo(
echo   T - Tiny   15 x 10
echo   S - Small  30 x 20
echo   M - Medium 40 x 25
echo   L - Large  47 x 32
echo   W - Wide   82 x 19
echo   N - Narrow 15 x 40
echo(
call :ask ">" !keys!
set "size=!size%key%!"
set /a "dispWidth=!width%key%!, dispHeight=!height%key%!"
call :loadHighScores
goto :saveUserPrefs
exit /b


::-------------------------------------
:controlOptions
cls
set "keys={Enter} T L R P"
if !moveKeys! equ 4 set "keys=!keys! U D"
                    echo Control Options:
                    echo(
                    echo   T - Type... = !moveKeys! keys
                    echo(
                    echo   L - Left... = !left!
                    echo   R - Right.. = !right!
if !moveKeys! equ 4 echo   U - Up..... = !up!
if !moveKeys! equ 4 echo   D - Down... = !down!
                    echo(
                    echo   P - Pause.. = !pause!
                    echo(
                    echo   {Enter} - Return to Main Menu
                    echo(
call :ask ">" !keys!
if !key! equ {Enter} goto :saveUserPrefs
if /i !key! equ T (
  if !moveKeys! equ 2 (set "moveKeys=4") else set "moveKeys=2"
  goto :controlOptions
)
set "option= LLeft RRight UUp DDown PPause"
for /f %%O in ("!option:* %key%=!") do (
  call :ask "Press a key for %%O:"
  for %%K in (0 1 2) do if "!key!" equ "!invalid:~%%K,1!" goto :controlOptions
  for %%C in (!upper!) do set "key=!key:%%C=%%C!"
  set "%%O=!key!"
)
goto :controlOptions


::-------------------------------------
:graphicOptions
set "toggleVT100="
cls
echo Graphic Options:
echo(
echo   B - Border...... = !bound!
echo   E - Empty space. = !space!
echo   H - snake Head.. = !head!
echo   S - Snake body.. = !body!
echo   F - Food........ = !food!
echo   D - Death....... = !death!
echo(
echo   V - VT100 mode.. = !vt%vt%!
echo(
echo   {Enter} - Rturn to Main Menu
echo(
call :ask ">" B E H S F D V {Enter}
if !key! equ {Enter} goto :saveUserPrefs
if /i !key! equ V (
  set /a "vt=^!vt"
  call :saveUserPrefs
  %sendCmd% quit
  exit 2
) else (
  set "option=-BBorder:bound:-EEmpty Space:space:-HSnake Head:head:-SSnake Body:body:-FFood:food:-DDeath:death:"
  for /f "tokens=1,2 delims=:" %%A in ("!option:*-%key%=!") do (
    call :ask "Press a key for %%A"
    for %%K in (0 1 2) do if "!key!" equ "!invalid:~%%K,1!" goto :graphicOptions
    set "%%B=!key!"
  )
)
goto :graphicOptions


::------------------------------------
:saveUserPrefs
(for %%V in (moveKeys up down left right space bound food head body death pause growth vt size dispWidth dispHeight) do echo %%V=!%%V!) >"%userPref%"
exit /b


::-------------------------------------
:initialize
cls
if defined replay (
  echo Replay Speed Options:
) else (
  echo Speed Options:
)
echo                       delay
echo    #   Description  (seconds)
echo   ---  -----------  ---------
for /l %%N in (1 1 6) do (
  set "delay=0!delay%%N!"
  set "desc=!desc%%N!           "
  echo    %%N   !desc:~0,11!    0.!delay:~-2!
)
echo(
call :ask "Pick a speed (1-6):" 1 2 3 4 5 6
set "difficulty=!desc%key%!"
set "delay=!delay%key%!"
set "diffCode=%key%"
echo %key% - %difficulty%
echo(
<nul set /p "=Initializing."
for %%A in (!configOptions!) do set "%%ASave=!%%A!"
if defined replay (
  %sendCmd% replay
  %sendCmd% !replay!
  call :waitForSignal
  set "replay=(REPLAY at !difficulty!)"
  set "size=%defaultSize%"
  set /a "dispWidth=defaultWidth, dispHeight=defaultHeight"
  :loadReplayConfig
  <&%keyStream% set /p "ln="
  if "!ln!" neq "END" set "!ln!" & goto :loadReplayConfig
  call :resize
)
set "axis=X"
set "xDiff=+1"
set "yDiff=+0"
set "empty="
set /a "PX=1, PY=height/2, FX=width/2+1, FY=PY, score=0, emptyCnt=0, t1=-1000000"
set "gameStart="
set "m=00"
set "s=00"
set "snakeX= %PX%"
set "snakeY= %PY%"
set "snakeX=%snakeX:~-2%"
set "snakeY=%snakeY:~-2%"
for /l %%Y in (0 1 %height%) do (
  <nul set /p "=."
  set "line%%Y="
  for /l %%X in (0,1,%width%) do (
    set "cell="
    if %%Y equ 0        set "cell=%bound%"
    if %%Y equ %height% set "cell=%bound%"
    if %%X equ 0        set "cell=%bound%"
    if %%X equ %width%  set "cell=%bound%"
    if %%X equ %PX% if %%Y equ %PY% set "cell=%head%"
    if not defined cell (
      set "cell=%space%"
      set "eX= %%X"
      set "eY= %%Y"
      set "empty=!empty!#!eX:~-2! !eY:~-2!"
      set /a emptyCnt+=1
    )
    if %%X equ %FX% if %%Y equ %FY% set "cell=%food%"
    set "line%%Y=!line%%Y!!cell!"
  )
)
set "replayFinished="
if defined replay (
  set "keys="
  set "hi=0"
  for /f "delims=:" %%A in ('findstr "^::" "%hiFile%!diffCode!.txt" 2^>nul') do set "hi=%%A"
  %HideCursor%
  cls
  (%draw%)
  call :delay 100
) else (
  if defined hi%diffCode% (set "hi=!hi%diffCode%!") else set "hi=0"
  cls
  (%draw%)
  >"!gameLog!" ( 
    for %%A in (!configOptions!) do (echo %%A=!%%A!)
    (echo END)
  )
  echo(
  if !moveKeys! equ 4 (
    echo Controls: !up!=up !down!=down !left!=left !right!=right !pause!=pause
  ) else (
    echo Controls: !left!=left !right!=right !pause!=pause
  )
  echo Avoid running into yourself (!body!!body!!head!^) or wall (!bound!^)
  echo Eat food (!food!^) to grow.
  echo(
  call :ask "Press a key to start..."
  %HideCursor%
  %sendCmd% go
)
set "pauseTime=0"
set "xDiff!up!=+0"
set "xDiff!down!=+0"
set "xDiff!left!=-1"
set "xDiff!right!=+1"
set "yDiff!up!=-1"
set "yDiff!down!=+1"
set "yDiff!left!=+0"
set "yDiff!right!=+0"
set "!up!Axis=Y"
set "!down!Axis=Y"
set "!left!Axis=X"
set "!right!Axis=X"
set "xTurn!left!=1"
set "xTurn!right!=-1"
set "yTurn!left!=-1"
set "yTurn!right!=1"
set "playerSpace=!space!!food!"
set ^"keys="!left!" "!right!" "!pause!"^"
set "newHi="
set "grow=0"
if !moveKeys! equ 4 set ^"keys=!keys! "!up!" "!down!"^"
if defined Up4 if not defined replay (
  %Up4%
  for /l %%N in (1 1 5) do (echo(                                             )
)
exit /b


::-------------------------------------
:waitForSignal
if not exist "%signal%" goto :waitForSignal
del "%signal%"
exit /b


::-------------------------------------
:loadHighScores
set "saveAvailable="
for /l %%N in (1 1 6) do (
  set "hi%%N="
  for /f "delims=:" %%A in ('findstr "^::" "%hiFile%%%N.txt" 2^>nul') do (
    set "hi%%N=%%A"
    set "saveAvailable=S"
  )
)
exit /b


::----------------------------
:resize the console window
set /a cols=dispWidth+1, lines=dispHeight+10, area=(dispWidth-2)*(dispHeight-2)
if %area% gtr 1365 (
  echo ERROR: Playfield area too large
  %sendCmd% quit
  exit
)
if %lines% lss 25 set lines=25
if %cols% lss 46 set cols=46
mode con: cols=%cols% lines=%lines%
set /a "width=dispWidth-1, height=dispHeight-1"
set "resize="
exit /b


::-------------------------------------
:fixLogs
setlocal enableDelayedExpansion
for %%F in (*.snake) do (
  ren "%%F" "%%F.txt"
  call :fixLog "%%F.txt"
)
pushd "%SaveLoc%"
for /f "delims=" %%F in ('dir /b SnakeHi*.txt 2^>nul') do (
  set "file=%%~nF"
  set "file=Snake1Hi!file:~-1!.txt"
  ren "%%F" "!file!"
  call :fixLog "!file!"
)
for /f "tokens=1* delims=eE" %%A in (
  'dir /b Snake*Hi*.txt ^| findstr /i "^Snake[0-9]"'
) do ren "Snake%%B" "SnakeMedium%%B"
popd
exit /b

:fixLog  filePath
>"%~1.new" (
  <"%~1" (
    for %%A in (diffCode difficulty moveKeys up down left right) do (
      set /p "val="
      (echo %%A=!val!)
    )
  )
  (echo growth=1)
  (echo END)
  more +7 "%~1"
)
move /y "%~1.new" "%~1" >nul
exit /b


::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:controller
:: Detects keypresses and sends the information to the game via a key file.
:: The controller has various modes of input that are activated by commands sent
:: from the game via a cmd file.
::
:: Modes:
::
::   hold   - No input, wait for command
::
::   go     - Continuously get/send key presses
::
::   prompt - Send {purged} marker to allow game to purge input buffer, then
::            get/send a single key press and hold
::
::   one    - Get/send a single key press and hold
::
::   replay - Copy a game log to the key file. The next line in cmd file
::            specifies name of log file to copy. During replay, the controller
::            will send an abort signal to the game if a key is pressed.
::
::   quit   - Immediately exit the controller process
::
:: As written, this routine incorrectly reports ! as ), but that doesn't matter
:: since we don't need that key. Both <CR> and Enter key are reported as {Enter}.
:: An extra character is appended to the output to preserve any control chars
:: when read by SET /P.

setlocal enableDelayedExpansion
for /f %%a in ('copy /Z "%~dpf0" nul') do set "CR=%%a"
set "cmd=hold"
set "inCmd="
set "key="
for /l %%. in () do (
  if "!cmd!" neq "hold" (
    for /f "delims=" %%A in ('xcopy /w "%~f0" "%~f0" 2^>nul') do (
      if not defined key set "key=%%A"
    )
    set "key=!key:~-1!"
    if !key! equ !CR! set "key={Enter}"
  )
  <&%cmdStream% set /p "inCmd="
  if defined inCmd (
    if !inCmd! equ quit exit
    set "cmd=!inCmd!"
    if !inCmd! equ replay (
      <&%cmdStream% set /p "file="
      type "!file!" >&%keyStream%
      copy nul "%signal%"
    )
    set "inCmd="
  )
  if defined key (
    if "!cmd!" equ "prompt" (echo {purged}.)
    if "!cmd!" equ "replay" (
      copy nul "%signal%" >nul
      set "cmd=go"
    ) else (echo(!key!.)
    if "!cmd!" neq "go" set "cmd=hold"
    set "key="
  )>&%keyStream%
)

====================================================================================================
Here begins the original post

For a while now I’ve been messing around with the concept of creating an arcade style game using only pure native batch. I ran across an interesting implementation of the classic SNAKE game (BATCHSNAKE.BAT) at http://www.youtube.com/watch?v=RblizDDxaBA
EDIT: I updated the link to point to the original author’s YouTube post. The prior link was by an impostor claiming credit

BATCHSNAKE.BAT uses an interesting technique to emulate the ability to detect a key press without pausing the game. The main game runs in one console window, and it launches a controller batch process in a new window that uses CHOICE to read keys. The controller process writes the key presses to a file, and the main game has stdin redirected to the file, so it can read the keys using SET /P. The nice feature is that SET /P reads the next key if available, but immediately moves on with no input if currently at the end of the file. What I don’t like is the reliance on two windows, it is confusing. Also, the implementation seems to sometimes drop key presses. BATCHSCRIPT.BAT also uses multiple batch scripts.

BATCHSNAKE.BAT works fairly well, and has a lot of nice features. But the screen has a lot of flicker, and the maximum speed is severely limited by the number of CALL statements, as well as to a lesser extent by the complexity of some logic and dependence of randomly finding a free space to locate the next piece of food. Performance on XP running via Win 7 Virtual PC is abysmal, and the controller process is somehow broken on the virtual PC (it launches but then closes).

I decided to create my own version of SNAKE using pure batch, concentrating on performance and controller reliability and ease of use. I chose not to implement features like high score, user defined maps, rocks, etc. Those features could easily be added.

Controller improvements
I put all code within one batch script (SNAKE.BAT). I launch the controller process using START /B so the controller runs in the same console window. The controller has various modes to allow the game to detect individual key presses, as well as to prompt and wait for input. The game communicates with the controller by writing commands to a command file that the controller then reads. Getting the correct behavior within a shared console window requires carefully constructed output redirection for the controller, as well as input redirection for the game.

The controller requires the CHOICE command. Unfortunately, the CHOICE command is not standard for XP. But XP users can still use the game if they download a port of the CHOICE command. SNAKE.BAT detects if the CHOICE command is available, and displays URLs where it may be downloaded if it is not available. The calling syntax for CHOICE varies depending on the version of CHOICE. SNAKE.BAT detects which version is available and sets up a simple «macro» to account for the version.

Speed improvements
This is a BIG topic. There are a number of important basic design principles.

1) Minimize use of GOTO

The main game loop is an infinite FOR /L loop. The infinite loop is exited via the EXIT command. The main game is launched from SNAKE.BAT via a new CMD.EXE so that necessary cleanup actions can take place after the loop exits. Use of GOTO for the main loop would slow down the program.

2) Minimize use of CALL

CALLed Batch «functions» with arguments are convenient, but the CALL mechanism is comparatively very slow. SNAKE.BAT dramatically improves speed via extensive use of batch «macros» with arguments. See Batch «macros» with arguments for more info on how to use macros. The link gives a good background on the macro concept. It provides a link to a more recent thread with major macro enhancements by jeb that more closely resemble the macros used in SNAKE.BAT, as well as a link to the initial thread where Ed Dyreen introduced the macro with arguments concept.

3) Encapsulate logic in data structures.

A lot of IF logic is avoided by defining variables with carefully constructed names, coupled with staged variable expansion. For example, if %%K contains the currently selected direction key (W A S or D), and xDiffW, xDiffA, xDiffS, and xDiffD contain the possible movement deltas for the x axis, then !xDiff%%K! will yield the currently selected movement x delta. The actual code uses SET /A to read the value of xDiff%%K directly without the need for delayed expansion.

4) Efficiently «plot» characters as «pixels»

The screen is composed of a set of environment variables holding fixed length strings that are ECHOed, one string per graphic line. Each character in a string represents a graphic «pixel». The variable name specifies the Y coordinate, and the position within the string the X coordinate. All «graphic» lines are created pixel by pixel just once upon initialization. From then on, a Plot macro uses simple SET substring operations to set the value of individual «pixels» as needed. There is no need to recompose the entire screen.

5) Snake definition

The snake is represented as variables containing a string of coordinates, one variable for X, and another for Y. The beginning of the string is the head, and the end is the tail. As the snake «moves» forward, simple SET substring operations are used to place a new coordinate at the front, and remove a coordinate from the rear.

6) Random food placement

A list of empty coordinates is maintained in a string variable so that food can be randomly placed without fear of colliding with an already occupied pixel. A random number between 0 and the count of available pixels is generated, and a SET substring operation is used to extract the available position. Then a SET search and replace is used to remove the newly occupied coordinates from the empty list. When the snake tail moves, the newly vacated pixel coordinates are appended to the end of the empty list.

7) Collision detection

Classic pixel probing is used to detect collisions. When the snake head is moving, a Test macro probes the value of the new position using a SET substring operation. The macro compares the current value against a list of allowable values. Disallowed values represent a collision.

8) Smooth motion

A fixed delay between each main loop iteration can result in jerky movement because the amount of work required by an iteration varies depending on context. More work equates to more time. Instead of introducing a fixed delay between each iteration, I note the time the previous movement ended, and then continuously check how much time has elapsed since the last movement. I only initiate movement when the delay time is exceeded. As long as the time spent on any one iteration is always less then the delay time, then the motion will always be smooth.

Results
I am extremely pleased with the speed and responsivenes of SNAKE.BAT. Setting the delay to 0 demonstrates just how fast the program executes — it is rediculously fast. A speed of 6 (delay = 30 milliseconds) is totally smooth on my Windows 7 machine, but it is so fast it is unplayable. I tend to play with a speed of 3 (delay = 100 milliseconds). At that speed, I get no flicker on my screen except for the top most border line. Faster speeds result in some flicker throughout the screen, but the motion remains smooth.

The game even works well when I run it on an XP virtual machine within Windows 7. There is much more flicker, but it still remains fairly smooth.

Possible Enhancements
Obviously things like high score lists, user defined maps, rocks, etc. that are available in the original BATCHSNAKE.BAT could be imlemented.

The following ideas interest me more. At one point I was considering implementing the following ideas, but I ran out of steam.

1) Increased play field size

The empy pixel list is maintained in a variable with a max string length of 8191 bytes. That restricts the total area of the play field. The snake definition variables also restrict the size, but they support a larger area then the empty list. Maximum playfield size could be increased by splitting the empty list and snake definition over multiple variables. But that complicates the management of those lists.

2) Introduce enemy snakes

If the food is not eaten within a time limit, then it can spawn into an enemy snake. The enemy snake(s) can eat food or chase you. Shorter snakes move faster than longer snakes. A small snake can nibble at the tail of a large snake. A large snake can swallow a small snake whole if eaten head first. Running into the side of a snake results in a collision (death). This sounds complicated, but the logic need not be all that complex. And I believe there is plenty of performance bandwidth left to handle the added complexity without hurting the animation quality.

So, here at last is the code for SNAKE.BAT
Embedded in a comment at the top are instructions for how to disable the awfull beep that occurs when an invalid key is pressed.

Code: Select all

:: Disable that awful beep (commands must be run with administrator privilege)
::   current session only:  net stop beep
::   permanently:  sc config beep start= disabled

@echo off
if "%~1" == "startGame" goto :game
if "%~1" == "startController" goto :controller


::------------------------------------------------------------
:: verify existence of CHOICE command
:: set up a macro appropriately depending on available version

set "choice="
2>nul >nul choice /c:yn /t 0 /d y
if errorlevel 1 if not errorlevel 2 set "choice=choice /cs"
if not defined choice (
  2>nul >nul choice /c:yn /t:y,1
  if errorlevel 1 if not errorlevel 2 set "choice=choice /s"
)
if not defined choice (
  echo ERROR: This game requires the CHOICE command, but it is missing.
  echo Game aborted. :(
  echo(
  echo A 16 bit port of CHOICE.EXE from FREEDOS is available at
  echo http://winsupport.org/utilities/freedos-choice.html
  echo(
  echo A 32 bit version from ??? suitable for 64 bit machines is available at
  echo http://hp.vector.co.jp/authors/VA007219/dkclonesup/choice.html
  echo(
  exit /b
)


::---------------------------------------------------------------------
:: setup some global variables used by both the game and the controller

set "keys=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
set "keyFile=key.txt"
set "cmdFile=cmd.txt"


::------------------------------------------
:: launch the game and the controller

copy nul "%keyFile%" >nul
start "" /b "%~f0" startController 9^>^>%keyFile% 2^>nul ^>nul
cmd /c "%~f0" startGame 9^<%keyFile% ^<nul
echo(

::--------------------------------------------------------------------------------
:: Upon exit, wait for the controller to close before deleting the temp input file

:close
2>nul (>>"%keyFile%" call )||goto :close
del "%keyFile%"
exit /b


::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:game
setlocal disableDelayedExpansion
title %~nx0
cls

::----------------------------
:: user configurable options

set "up=W"
set "down=S"
set "left=A"
set "right=D"

set "width=40"   max=99
set "height=25"  max=99
:: max playing field: (width-2)*(height-2) <= 1365

::----------------------------
:: resize the console window

set /a cols=width+1, lines=height+10, area=(width-2)*(height-2)
if %area% gtr 1365 (
  echo ERROR: Playfield area too large
  >"%cmdFile%" (echo quit)
  exit
)
if %lines% lss 14 set lines=14
if %cols% lss 46 set cols=46
mode con: cols=%cols% lines=%lines%

::----------------------------
:: define variables

set "spinner1=-"
set "spinner2=\"
set "spinner3=|"
set "spinner4=/"
set "spinner= spinner1 spinner2 spinner3 spinner4 "

set "space= "
set "bound=#"
set "food=+"
set "head=@"
set "body=O"
set "death=X"
set "playerSpace=%space%%food%"

set "xDiff%up%=+0"
set "xDiff%down%=+0"
set "xDiff%left%=-1"
set "xDiff%right%=+1"

set "yDiff%up%=-1"
set "yDiff%down%=+1"
set "yDiff%left%=+0"
set "yDiff%right%=+0"

set "%up%Axis=Y"
set "%down%Axis=Y"
set "%left%Axis=X"
set "%right%Axis=X"

set "delay1=20"
set "delay2=15"
set "delay3=10"
set "delay4=7"
set "delay5=5"
set "delay6=3"
set "delay0=0"

set "desc1=Sluggard"
set "desc2=Crawl"
set "desc3=Slow"
set "desc4=Normal"
set "desc5=Fast"
set "desc6=Insane"
set "desc0=Unplayable"

set "spinnerDelay=3"

set /a "width-=1, height-=1"


::---------------------------
:: define macros

::define a Line Feed (newline) string (normally only used as !LF!)
set LF=^


::Above 2 blank lines are required - do not remove

::define a newline with line continuation
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"

:: setErr
:::  Sets the ERRORLEVEL to 1
set "setErr=(call)"

:: clrErr
:::  Sets the ERRORLEVEL to 0
set "clrErr=(call )"


:: getKey  ValidKeys
::: Check for keypress. Only accept keys listed in ValidKeys
::: Return result in Key variable. Key is undefined if no valid keypress.
set getKey=%\n%
for %%# in (1 2) do if %%#==2 (for /f "eol= delims= " %%1 in ("!args!") do (%\n%
  set "validKeys=%%1"%\n%
  set "key="%\n%
  ^<^&9 set /p "key="%\n%
  if defined key if "!key!" neq ":" (%\n%
    set /a key-=1%\n%
    for %%K in (!key!) do set "key=!keys:~%%K,1!"%\n%
  )%\n%
  for %%K in (!key!) do if "!validKeys:%%K=!" equ "!validKeys!" set "key="%\n%
)) else set args=


:: draw
:::  draws the board
set draw=%\n%
cls%\n%
for /l %%Y in (0,1,%height%) do echo(!line%%Y!%\n%
echo Speed=!Difficulty!%\n%
echo Score=!score!


:: test  X  Y  ValueListVar
:::  tests if value at coordinates X,Y is within contents of ValueListVar
set test=%\n%
for %%# in (1 2) do if %%#==2 (for /f "tokens=1-3" %%1 in ("!args!") do (%\n%
  for %%A in ("!line%%2:~%%1,1!") do if "!%%3:%%~A=!" neq "!%%3!" %clrErr% else %setErr%%\n%
)) else set args=


:: plot  X  Y  ValueVar
:::  places contents of ValueVar at coordinates X,Y
set plot=%\n%
for %%# in (1 2) do if %%#==2 (for /f "tokens=1-3" %%1 in ("!args!") do (%\n%
  set "part2=!line%%2:~%%1!"%\n%
  set "line%%2=!line%%2:~0,%%1!!%%3!!part2:~1!"%\n%
)) else set args=


::--------------------------------------
:: start the game
setlocal enableDelayedExpansion
call :initialize


::--------------------------------------
:: main loop (infinite loop)
for /l %%. in (1 0 1) do (

  %=== compute time since last move ===%
  for /f "tokens=1-4 delims=:.," %%a in ("!time: =0!") do set /a "t2=(((1%%a*60)+1%%b)*60+1%%c)*100+1%%d-36610100, tDiff=t2-t1"
  if !tDiff! lss 0 set /a tDiff+=24*60*60*100

  if !tDiff! geq !delay! (
    %=== delay has expired, so time for movement ===%

    %=== establish direction ===%
    %getKey% ASDW
    for %%K in (!key!) do if "!%%KAxis!" neq "!axis!" (
      set /a "xDiff=xDiff%%K, yDiff=yDiff%%K"
      set "axis=!%%KAxis!"
    )

    %=== erase the tail ===%
    set "TX=!snakeX:~-2!"
    set "TY=!snakeY:~-2!"
    set "snakeX=!snakeX:~0,-2!"
    set "snakeY=!snakeY:~0,-2!"
    %plot% !TX! !TY! space

    %=== compute new head location and attempt to move ===%
    set /a "X=PX+xDiff, Y=PY+yDiff"
    set "X= !X!"
    set "Y= !Y!"
    set "X=!X:~-2!"
    set "Y=!Y:~-2!"
    (%test% !X! !Y! playerSpace) && (

      %=== move successful ===%

      %=== remove the new head location from the empty list ===%
      for %%X in ("!X!") do for %%Y in ("!Y!") do set "empty=!empty:#%%~X %%~Y=!"

      (%test% !X! !Y! food) && (
        %=== moving to food - eat it ===%

        %=== restore the tail ===%
        %plot% !TX! !TY! body
        set "snakeX=!snakeX!!TX!"
        set "snakeY=!snakeY!!TY!"

        %=== increment score and locate and draw new food ===%
        set /a "score+=1, F=(!random!%%(emptyCnt-=1))*6+1"
        for %%F in (!F!) do (%plot% !empty:~%%F,5! food)

      ) || (
        %=== moving to empty space ===%

        %=== add the former tail position to the empty list ===%
        set "empty=!empty!#!TX! !TY!"
      )

      %=== draw the new head ===%
      if defined snakeX (%plot% !PX! !PY! body)
      %plot% !X! !Y! head

      %=== Add the new head position to the snake strings ===%
      set "snakeX=!X!!snakeX!"
      set "snakeY=!Y!!snakeY!"
      set "PX=!X!"
      set "PY=!Y!"

      %draw%

    ) || (

      %=== failed move - game over ===%
      %plot% !TX! !TY! body
      call :spinner !PX! !PY! death
      %draw%
      echo(
      call :ask "Would you like to play again? (Y/N)" YN
      if /i "!key!" equ "N" (
        >"%cmdFile%" (echo quit)
        exit
      ) else (
        call :initialize
      )
    )

    set /a t1=t2
  )
)

:ask  Prompt  ValidKeys
:: Prompt for a keypress. ValidKeys is a list of acceptable keys
:: Wait until a valid key is pressed and return result in Key variable
>"%cmdFile%" (echo prompt)
<nul set /p "=%~1 "
:purge
(%getKey% :)
if not defined key goto :purge
:getResponse
(%getKey% %2)
if not defined key (
  >"%cmdFile%" (echo one)
  goto :getResponse
)
exit /b


:spinner  X  Y  ValueVar
set /a d1=-1000000
for /l %%N in (1 1 5) do for %%C in (%spinner%) do (
  call :spinnerDelay
  %plot% %1 %2 %%C
  %draw%
)
call :spinnerDelay
(%plot% %1 %2 %3)
exit /b

:spinnerDelay
for /f "tokens=1-4 delims=:.," %%a in ("!time: =0!") do set /a "d2=(((1%%a*60)+1%%b)*60+1%%c)*100+1%%d-36610100, dDiff=d2-d1"
if %dDiff% lss 0 set /a dDiff+=24*60*60*100
if %dDiff% lss %spinnerDelay% goto :spinnerDelay
set /a d1=d2
exit /b


::-------------------------------------
:initialize
cls

echo Speed Options:
echo                       delay
echo    #   Description  (seconds)
echo   ---  -----------  ---------
echo    1   Sluggard        0.20
echo    2   Crawl           0.15
echo    3   Slow            0.10
echo    4   Normal          0.07
echo    5   Fast            0.05
echo    6   Insane          0.03
echo    0   Unplayable      none
echo(
call :ask "Pick a speed (1-6, 0):" 1234560
set "difficulty=!desc%key%!"
set "delay=!delay%key%!"
echo %key% - %difficulty%
echo(
<nul set /p "=Initializing."
set "axis=X"
set "xDiff=+1"
set "yDiff=+0"
set "empty="
set /a "PX=1, PY=height/2, FX=width/2+1, FY=PY, score=0, emptyCnt=0, t1=-1000000"
set "snakeX= %PX%"
set "snakeY= %PY%"
set "snakeX=%snakeX:~-2%"
set "snakeY=%snakeY:~-2%"
for /l %%Y in (0 1 %height%) do (
  <nul set /p "=."
  set "line%%Y="
  for /l %%X in (0,1,%width%) do (
    set "cell="
    if %%Y equ 0        set "cell=%bound%"
    if %%Y equ %height% set "cell=%bound%"
    if %%X equ 0        set "cell=%bound%"
    if %%X equ %width%  set "cell=%bound%"
    if %%X equ %PX% if %%Y equ %PY% set "cell=%head%"
    if not defined cell (
      set "cell=%space%"
      set "eX= %%X"
      set "eY= %%Y"
      set "empty=!empty!#!eX:~-2! !eY:~-2!"
      set /a emptyCnt+=1
    )
    if %%X equ %FX% if %%Y equ %FY% set "cell=%food%"
    set "line%%Y=!line%%Y!!cell!"
  )
)
(%draw%)
echo(
echo Movement keys: %up%=up %down%=down %left%=left %right%=right
echo Avoid running into yourself (%body%%body%%head%) or wall (%bound%)
echo Eat food (%food%) to grow.
echo(
call :ask "Press any alpha-numeric key to start..." %keys%
>"%cmdFile%" (echo go)
exit /b


::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:controller

setlocal enableDelayedExpansion
set "cmd=hold"
set "key="
for /l %%. in (1 0 1) do (
  if "!cmd!" neq "hold" (
    %choice% /n /c:!keys!
    set "key=!errorlevel!"
  )
  if exist "%cmdFile%" (
    <"%cmdFile%" set /p "cmd="
    del "%cmdFile%"
  )
  if "!cmd!" equ "quit" exit
  if defined key (
    if "!cmd!" equ "prompt" >&9 (echo :)
    >&9 (echo !key!)
    if "!cmd!" neq "go" set "cmd=hold"
    set "key="
  )
)

EDIT: Added /C: option to the test for CHOICE to account for some internationalization issues
EDIT2: Added comma to DELIMS option when parsing time to support European decimal point
EDIT3: Changed 2nd CHOICE test to wait 1 second to support downloaded 16 bit version and fixed disappearing food
bug

EDIT4: One more CHOICE test change to account for fact that 16 bit version returns 0 if invalid arguments

Dave Benham

Introduction: Windows Command Prompt Text Game

If anybody here remembers Zork, then you are the true Geek. I wanted to have something to entertain my self while on the computer in class, without them checking my history and busting me. So, I created this Game Called Option B.
It’s just a lot of files titled with different option. You have to have sleep, food, and certain items to survive the game.

I’ll Show you how to make it.

Step 1: Let’s Get Started.

Ok, So I figured that just naming folder with different options would be just fine for this game.

First open up your C: drive, or create the game on a flash drive. If you go with flash drives, you can create multiple games.
Make a new folder, and title it with whatever you want.

Step 2: Start Adding Moves to the Game

After you have put in a folder for the game, start adding moves.

I had:
Move forward
Move Back
Move Left
Move Right

after each of these add a correspond action, for example, If you go forward, you have the option to climb a tree, giving the skill of surveillance. 

Also, if you go back, you get a message that you cannot go back.

Step 3: Add Items to Locations.

In Option B you must have food, sleep, and other items to move forward in the game. In all the houses I put assorted items, such as Knives, flashlights, coats, extra clothes, disguises, ect. To move forward, for example, you might need a knife to open a lock to a shed containing essentials. 

Add what you like to make a fun, or challenging game.

Step 4: Add People

If you can’t figure out how to get into a lock, or get certain items, you may need help. Adding people can be like adding little hints to move the game along. In all of the houses, I have at least one person. Some are hostile, so you will need food and sleep to recover. Others, will give very helpful hints on moving forward.

add people to every building, and sometimes in the open. They will really make the game alive.

Step 5: Make an Ending

All games must come to an end. In my game, you reach a car, enabling you to leave the town. 
Make the game ending make sense, also logical.

Step 6: How to Play

For people who have not used cmd.exe, it will be difficult at first, but here are the command you need to know:

@ To Start, you should see just C: or what ever drive you are using, not anything else. 

@ To see what files are visible, type in Dir, this will show a directory.

@ to open a file or directory, type in cd and then the file name

@ to back up, or leave a file, type in cd.. (with periods.)

@ You can change the color of command prompt by typing in Color dir, this will show a menu of colors to choose from.

These are the basice commands needed to navigate through the game.

See pictures for better explination of the commands.

Step 7: End!

You have just written out a full game that can be played on Windows Command promt, or even just by going through files. No Internet needed!

Good job! Leave your game ideas below.

  • Игры логические для windows 10
  • Игры для девочек на windows 7 торрент
  • Игры для планшета на windows 10 скачать торрент
  • Игры гонки для ноутбука windows 10
  • Игры которые пойдут на windows 7