Привет!
Сегодня я опишу в подробностях, как я сделал игру в командной строке, и насколько хороша она получилась.
Откуда идея?
Я вдохновился идеей сделать что-то простое на первый взгляд, но в тоже время интересное в плане разработки. Мне в голову пришла мысль, сделать игру в консоли, это интересно в плане разработки, и посмотреть на это будет интересно даже просто со стороны, как например на эту игру.
Игровой движок
Итак, начнем с того как игра устроена в корне, и какова ее идея работы.
Сначала я определился с тем, как будет выводится игровой мир в консоль. Понял, что для вывода игровых объектов, нам нужен список, который хранит в себе другие списки, которые хранят в себе символы, которые в последующем выводятся на игровое поле циклом for
.
Вот таким кодом:
for line_words in OUTPUT_IMAGE:
for word in line_words:
print(word, end="")
print("\n", end="")
Здесь мы рисуем все символы из списка, и переходим на новую строку, чтоб нарисовать следующий список символов.
Вот так выглядит переменная, которая хранит списки символов:
Тут 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="")
И вот, мы нарисовали объект на нашем игровом поле. Правда координаты X и Y получились не классическими. Во первых, мы указываем сначала Y, потом X, что не совсем по классике, во вторых, координата Y должна увеличиваться чтоб поднять объект, у нас же наоборот, она должна уменьшатся.
График X и Y в игре:
Эту особенность тоже придется учитывать в дальнейшем, когда мы будем делать столкновение объектов в консоли.
Теперь мы можем попробовать перемещать наш объект по игровому полю, т.е. создавать движение.
Нам понадобится очищать консоль, для того чтобы стирать старую картинку игрового поля.
Это мы сделаем командой:
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 = [
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
]
Теперь у нас есть возможность распределять объекты по полю.
Правда эти объекты слишком примитивы, и надо бы научится рисовать сложные объекты по типу игроков, домов, еды…
Для того чтобы нарисовать сложный объект, нам нужно понять и придумать, как нарисовать объект указав лишь один раз его 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 = [
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
]
И вот что у нас получилось:
абсолютно также как и шарик который мы рисовали, его можно двигать по оси 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 = [
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
[".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],
]
И вот, у нас уже двигает игрок по карте.
Тут мы сделали уже многое, уже есть игрок, уже есть карта, и казалось бы, уже можно сделать игру, но нет. Нам нужна функция высчета столкновений объектов, ведь какая это игра без взаимодействий объектов. Поэтому приступим.
Для начала нам нужно сделать функцию получение широты и высоты объекта, для того чтобы расчитать его хитбокс.
Итак, функцию я решил сделать по такой логике:
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 квадрата, которые сталкиваются, и по этой картинке можно придумать условие по которому будет высчитано столкновение
Для простоты понимания я нарисовал хитбоксы, Т.Е. квадраты:
Логика на словах
Для вычисления мы подаем
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, но вместо y
— x
, а вместо h
— w
.
если:
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
если нет.
Я нарисовал дополнительно куб на нашем игровом поле, для того чтобы игроку было с кем сталкиваться.
И попробовал как работает функция высчета столкновения.
Вот игрок соприкасается и кубом:
А вот нет соприкасаются:
Код соприкосновения
Это полный код соприкосновения/не соприкосновения:
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
Откройте Блокнот. Блокнот — это бесплатный текстовый редактор, включаемый во все версии Windows. Используйте Блокнот для ввода программного кода. Чтобы открыть Блокнот:
- Откройте меню «Пуск»
.
- Введите блокнот.
- Нажмите «Блокнот» в верхней части меню.
- Откройте меню «Пуск»
-
2
Введите название игры. Скопируйте следующий текст в Блокнот — вместо [Название] введите название игры, а затем нажмите ↵ Enter:[1]
@echo off title [Название]
-
3
Определитесь с цветом текста и фона игры. Чтобы указать цвет, нужно ввести код в формате «0A», где «0» — это цвет фона, а «A» — это цвет текста. Коды распространенных цветов:[2]
- Цвет текста — используйте буквы A, B, C, D, E или F для обозначения светло-зеленого, светло-синего, светло-красного, светло-фиолетового, светло-желтого или белого цвета, соответственно.
- Цвет фона — используйте цифры 0, 1, 2, 3, 4, 5, 6, 7, 8 или 9 для обозначения черного, синего, зеленого, морской волны, красного, фиолетового, желтого, белого, серого или голубого цвета, соответственно.
- Например, чтобы создать белый текст на черном фоне, введите код «0F».
-
4
Задайте цвета игры. Скопируйте следующий текст в Блокнот — вместо «0A» введите нужную комбинацию цветов фона и текста, а затем нажмите ↵ Enter:
@echo off title Онлайн-игра color 0A if "%1" neq "" ( goto %1)
-
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
Задайте опцию «Выход». Так игроки смогут выйти из командной строки. Скопируйте следующий текст в Блокнот, а затем нажмите ↵ Enter:
:Exit cls echo Спасибо за игру! pause exit /b
-
7
Задайте опцию «Благодарность». Скопируйте следующий текст в Блокнот (вместо [Название] введите название игры), а затем нажмите ↵ Enter:
:Credits cls echo Благодарность echo. echo Спасибо за то, что играли в [Название]! pause goto Menu
-
8
Задайте опцию «Пуск». Она позволит игрокам начать новую игру:
:Start_1 cls echo О нет! Вы окружены врагами. echo Их пятеро, и все они вооружены. echo Если вы сразитесь с ними, у вас есть шансы на победу. set /p answer=Сразиться или убежать? if %answer%==сразиться goto Fight_1 if %answer%==убежать goto Run_1 pause
-
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
Откройте меню Файл. Оно находится в верхнем левом углу Блокнота.
-
11
Нажмите Сохранить как. Эта опция находится в меню «Файл». Откроется окно «Сохранить как».
-
12
Введите имя файла, а затем введите расширение «.bat». В текстовом поле «Имя файла» в нижней части окна введите любое имя файла, а затем введите расширение .bat, чтобы игра сохранилась в виде пакетного файла.
- Например, чтобы назвать игру «Dungeon Crawl», введите Dungeon Crawl.bat.
-
13
Измените формат файла. Откройте меню «Тип файла» в нижней части окна, а затем выберите в нем «Все файлы».
-
14
Сохраните игру на рабочем столе. Для этого нажмите «Рабочий стол» на левой панели. Возможно, сначала вам придется прокрутить панель вверх или вниз, чтобы найти папку «Рабочий стол».
-
15
Нажмите Сохранить. Это кнопка в правом нижнем углу окна. Игра будет сохранена как BAT-файл.
-
16
Запустите игру. Дважды щелкните по BAT-файлу, чтобы открыть игру в командной строке, а затем следуйте инструкциям на экране.
- Например, нажмите «1», чтобы начать игру.
-
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.