LoginSignup
4
3

More than 1 year has passed since last update.

Pythonとcursesで昔懐かしキャラクタゲームを再現してみた。

Last updated at Posted at 2019-07-01

40年ほど前の、CBM-3032にあった、cursorという同人ソフトのキャラクタゲームをpythonでリライトしてみました。名前も忘れたので、「Asterisks」と、しました。
説明も、何もなしですが、動かすとゲームが始まります。
プレイヤーは「O」テンキーの1〜9の方向に動きます。
フルキーでも、「jkluio789」と、「↑↓←→」に、対応しています。
「q」キーで、いつでも抜けられます。

目的は、複数の敵、「*」を、ブロック「B」で、八方を囲み、閉じ込めることです。
全ての敵を閉じ込めたら、「You Win」、敵に接触したら、「You Lose」となります。
デフォルト設計では、かなり易しめに作ってありますが、呼び出し時のオプションで、
敵の数とブロックの数を変えられます。

ubuntu pythonで、昔のゲームを作るポイントは5つ。
①cursesで、tty出力をする。
②locale.setlocale()で、cursesで、UTF-8を使えるようにしておく
③待ち時間なしのgetch()で、キー入力をする。
④time.sleep()で、全体の処理速度を落とす。
⑤仮想vram(ここでは、vvram[][])を使う。
です。

待ち時間なしのキー入力は、Qiitaの記事 Pythonでキー入力待ちをしないキー入力 を参考にしました。

pygameモジュールは使っていません。

ast.py
#!/usr/bin/python3

import argparse
import curses
import random
import locale
import time
import select
import termios
import sys
import os
import fcntl

parser = argparse.ArgumentParser(
    description='Retro game \'Asterisks\'', add_help=True)
parser.add_argument('-b', help='number of blocks', type=int, default=400)
parser.add_argument('-e', help='number of enemies', type=int, default=5)
args = parser.parse_args()

asts = args.e
blocks = args.b


def getkey():
    fno = sys.stdin.fileno()
    attr_old = termios.tcgetattr(fno)
    attr = termios.tcgetattr(fno)
    attr[3] = attr[3] & ~termios.ECHO & ~termios.ICANON  # & ~termios.ISIG
    termios.tcsetattr(fno, termios.TCSADRAIN, attr)
    fcntl_old = fcntl.fcntl(fno, fcntl.F_GETFL)
    fcntl.fcntl(fno, fcntl.F_SETFL, fcntl_old | os.O_NONBLOCK)

    try:
        c = sys.stdin.read(3)
        if c=='^[0A': #ここで、^[はエスケープキーのコードを表します。私はviで入力したのですが、qiitaにコピペすると正しく表示されませんので、皆さん、エディタで^[をエスケープキーのコードに置き換えて下さい。
           c='8'
        elif c=='^[0B':
           c='k'
        elif c=='^[0D':
           c='u'
        elif c=='^[0C':
           c='o'
    finally:
        fcntl.fcntl(fno, fcntl.F_SETFL, fcntl_old)
        termios.tcsetattr(fno, termios.TCSANOW, attr_old)

    return c


vvram = [[' ' for i in range(24)] for j in range(40)]
astx = [0]*asts
asty = [0]*asts
playerx = 20
playery = 12

locale.setlocale(locale.LC_ALL, '')


def ps(x, y):
    vvram[x][y] = ' '
    stdscr.addch(y, x*2, ' ')


def pp(x, y):
    vvram[x][y] = 'o'
    stdscr.addch(y, x*2, '')


def pb(x, y):
    vvram[x][y] = 'B'
    stdscr.addstr(y, x*2, '', curses.A_REVERSE)


def pw(x, y):
    vvram[x][y] = 'W'
    stdscr.addch(y, x*2, '')


def pa(x, y):
    vvram[x][y] = '*'
    stdscr.addch(y, x*2, '')


def initscreen():
    stdscr.clear()

    vvram = [[' ' for i in range(24)] for j in range(40)]

    wy = 0
    for wx in range(40):
        pw(wx, wy)
        pw(wx, wy+22)
    wx = 0
    for wy in range(22):
        pw(wx, wy)
        pw(wx+39, wy)

    for block in range(blocks):
        x = random.randint(1, 38)
        y = random.randint(1, 21)
        pb(x, y)

    pp(playerx, playery)

    for a in range(asts):
        while True:
            x = random.randint(1, 38)
            y = random.randint(1, 21)
            if vvram[x][y] == ' ':
                break
        pa(x, y)
        astx[a] = x
        asty[a] = y
    stdscr.refresh()


def move_ast():
    for i in range(asts):
        x = astx[i]+random.randint(-1, 1)
        y = asty[i]+random.randint(-1, 1)
        c = vvram[x][y]
        if c == ' ' or c == 'o':
            ps(astx[i], asty[i])
            astx[i] = x
            asty[i] = y
            pa(x, y)


def move_blocks(dx, dy):
    x = playerx+dx
    y = playery+dy
    while True:
        x += dx
        y += dy
        c = vvram[x][y]
        if (c != 'B'):
            break
    if c == ' ':
        pb(x, y)
        return(' ')
    return('W')


def move_player(k):
    global playerx, playery
    if k == '7':
        dx = -1
        dy = -1
    elif k == '8':
        dx = 0
        dy = -1
    elif k == '9':
        dx = 1
        dy = -1
    elif k == 'u' or k == '4':
        dx = -1
        dy = 0
    elif k == 'o' or k == '6':
        dx = 1
        dy = 0
    elif k == 'j' or k == '1':
        dx = -1
        dy = 1
    elif k == 'k' or k == '2':
        dx = 0
        dy = 1
    elif k == 'l' or k == '3':
        dx = 1
        dy = 1
    else:
        dx = 0
        dy = 0
    c = vvram[playerx+dx][playery+dy]
    if (c == 'B'):
        c = move_blocks(dx, dy)
    if (c == ' ' or c == 'B'):
        ps(playerx, playery)
        playerx += dx
        playery += dy
        pp(playerx, playery)


def lose_check():
    f = 0
    for i in range(asts):
        f += 1 if (astx[i] == playerx and asty[i] == playery) else 0
    return(f)


def game_finish():
    for i in range(asts):
        if (vvram[astx[i]-1][asty[i]-1] == ' '
            or vvram[astx[i]][asty[i]-1] == ' '
            or vvram[astx[i]+1][asty[i]-1] == ' '
            or vvram[astx[i]-1][asty[i]] == ' '
            or vvram[astx[i]+1][asty[i]] == ' '
            or vvram[astx[i]-1][asty[i]+1] == ' '
            or vvram[astx[i]][asty[i]+1] == ' '
                or vvram[astx[i]+1][asty[i]+1] == ' '):
            return(0)
    return(1)


def mainloop():
    x = 0
    while True:
        k = getkey()
        if k == 'q':
            return
        move_player(k)
        if x % 4 == 0:
            move_ast()
        g = 1
        if (lose_check()):
            stdscr.addstr(0, 0, "You Lose!! Game Over.")
            g = 0
        if (game_finish()):
            stdscr.addstr(0, 0, "You Win!! Game Over.")
            g = 0
        if g == 0:
            stdscr.refresh()
            while True:
                k = getkey()
                if k == 'q':
                    return
        stdscr.refresh()

        time.sleep(0.1)
        x += 1


stdscr = curses.initscr()
stdscr.keypad(True)
curses.curs_set(0)
initscreen()
mainloop()
curses.endwin()

4
3
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3