pygameを使って、ジョイパッド入力だけを取得する(Python)

#気軽にジョイスティックを使いたい
と思いませんか？
…思わないですか、じゃあ終了です。
#それだと終了してしまうので
思うことにしましょう。してください。
ジョイスティック入力を使用すれば、ウインドウやターミナルがアクティブになってる必要がありません。便利でしょう？、便利だと言え。
#pygameは標準
で、Pythonと言えば[pygame](https://www.pygame.org/ "pygame公式")なんで、[pygameを使います](https://pypi.org/project/pygame/ "pygame - Pypi")。
本当はDirectInputを使っても良かったのですが、~~使い方がさっぱりわからなかった~~マルチプラットフォームじゃなくなるのでpygameを使いました。pygameがWindows以外でどう動くのかしらんけど。
#とりあえずググると
pygameでjoystick入力だけを使いたくても、画面ださないといけないみたいなことがあちこちに書かれていましたが、画面ださなくても入力を取得しているソースを書いてある所がありました。それを元に作ってみます。
#ソースコード
~~~Python:joy.py
import pygame
import threading, time, copy

as_xbox = ['A','B','X','Y','LT','RT','LB','RB','LS','RS','BACK','START']
as_nes = ['A','B','SELECT','START']
as_snes = ['A','B','X','Y','L','R','SELECT','START']
as_ps4 = ['CIRCLE','CROSS','TRIANGLE','SQUARE','L1','R1','L2','R2','L3','R3']

def assign(status , assignlist = []):
    pushlist = []
    if len(status.axis) != 0:
        if status.axis[0] < 0:
            pushlist.append('left')
        elif status.axis[0] > 0:
            pushlist.append('right')
        if status.axis[1] < 0:
            pushlist.append('up')
        elif status.axis[1] > 0:
            pushlist.append('down')

    if len(status.hat) != 0:
        if status.hat[0] < 0:
            pushlist.append('left')
        elif status.hat[0] > 0:
            pushlist.append('right')
        if status.hat[1] < 0:
            pushlist.append('down')
        elif status.hat[1] > 0:
            pushlist.append('up')

    if len(assignlist) == 0:
        for i,j in enumerate(status.button):
            if j == 1:
                pushlist.append(str(i))
    else:
        for i,j in enumerate(assignlist):
            if i < len(status.button):
                if status.button[i] == 1:
                    pushlist.append(j)
    return pushlist

def reset():
    pygame.init()
    pygame.joystick.init()
    global joysticks
    global interval
    joysticks = pygame.joystick.get_count()
    interval = 1/60

reset()

def numjoystick():
    return joysticks

class Joy:
    def __init__(self, id):
        self.joystick = pygame.joystick.Joystick(id)
        self.joystick.init()
        self.id = id
        self.name = self.joystick.get_name()
        self.buttons = self.joystick.get_numbuttons()
        self.axes = self.joystick.get_numaxes()
        self.hats = self.joystick.get_numhats()
        self._status = status(self.id, self.name, self.axes, self.buttons, self.hats)
        self._bstatus = status(self.id, self.name, self.axes, self.buttons, self.hats)
        self._oneshot = status(self.id, self.name, self.axes, self.buttons, self.hats)
        self._th = False
        self.clearoneshot = True

    def start(self, callback = ''):
        self._callback = callback

        self._th = True
        polling = threading.Thread(target=self._polling)
        polling.setDaemon(True)
        polling.start()

    def _polling(self):
        stat = self._status
        bstat = self._bstatus
        ost = self._oneshot

        while self._th:
            s = False
            time.sleep(interval)
            bstat.axis = copy.deepcopy(stat.axis)
            for i in range(self.axes):
                stat.axis[i] = self.joystick.get_axis(i)
                if ost.axis[i] != 0.0:
                    if stat.axis[i] == 0.0:
                        ost.axis[i] = 0.0
                    elif stat.axis[i] < 0.0:
                        ost.axis[i] = min(ost.axis[i],stat.axis[i])
                    else:
                        ost.axis[i] = max(ost.axis[i],stat.axis[i])
                if bstat.axis[i] == 0.0 and stat.axis[i] != 0.0:
                    ost.axis[i] = stat.axis[i]
                    s = True

            bstat.button = copy.deepcopy(stat.button)
            for i in range(self.buttons):
                stat.button[i] = self.joystick.get_button(i)
                if bstat.button[i] == 0 and stat.button[i] == 1:
                    ost.button[i] += 1
                    s = True

            bstat.hat = copy.deepcopy(stat.hat)
            for i in range(self.hats):
                hat = self.joystick.get_hat(i)
                stat.hat[i*2] = hat[0]
                stat.hat[i*2+1] = hat[1]
                for j in [i*2,i*2+1]:
                    if bstat.hat[j] == 0 and stat.hat[j] != 0:
                        ost.hat[j] = stat.hat[j]
                        s = True

            pygame.event.pump()
            if s and self._callback != '':
                self._callback(self._status)

    def stop(self):
        self._th = False

    def get_status(self):
        return self._status

    def get_pushed(self):
        st = copy.deepcopy(self._oneshot)
        if self.clearoneshot:
            self._oneshot.axis = [0.0] * self.axes
            self._oneshot.button = [0] * self.buttons
            self._oneshot.hat = [0] * self.hats * 2
        return st

    def clear_pushed(self):
        self._oneshot.axis = [0.0] * self.axes
        self._oneshot.button = [0] * self.buttons
        self._oneshot.hat = [0] * self.hats * 2

    def namecheck(self, name):
        return name == self.name

    def quit(self):
        self._th = False

class status:
    def __init__(self,id, name , axes, buttons, hats):
        self.id = id
        self.name = name
        self.button = [0] * buttons
        self.axis = [0] * axes
        self.hat = [0] * hats * 2

if __name__ == '__main__':
    print('Joy Module')

~~~
Joyと言うクラスと、いくつかの関数で出来ています
#使い方
###関数
#####num = joy.numjoystick()
接続されたジョイスティックの数を返します
#####joy.reset()
初期化します。モジュールのインポート時に呼び出されています。
これ以外ではジョイスティックの接続数が変わったときなどに使用します。また、この関数内のintervalの値を変えるとジョイスティックの入力をどれくらいの頻度で検出するか変えられます。
#####joy.assign( status, [assignlist])
後述するstatusは全ての軸、全てのボタン、全てのハットスイッチの状態が押されているか押されていないかで返されます。
これを、押されているボタンのみの配列に直します。
assignlistは省略可能で、省略した場合、ボタンは0～の数字です
以下の指定がある場合、それぞれのボタン名が返されます
また、ハットスイッチと１番目の軸は同じ物とされ、"up","down","left","right"とデジタル入力化されます。２番目以降の軸、ハットスイッチは無視されます。
<dl>
  <dt>joy.as_nes</dt>
  <dd>'A','B','SELECT','START'</dd>
  <dt>joy.as_snes</dt>
  <dd>'A','B','X','Y','L','R','SELECT','START'</dd>
  <dt>joy.as_ps4</dt>
  <dd>'CIRCLE','CROSS','TRIANGLE','SQUARE','L1','R1','L2','R2','L3','R3'</dd>
  <dt>joy.as_xbox</dt>
  <dd>'A','B','X','Y','LT','RT','LB','RB','LS','RS','BACK','START'</dd>
</dl>
**注意:**どのタイプのジョイスティックが接続されているかを判別はしていません。16ボタンのコントローラを接続しているときにassignlistをjoy.as_nesにすると、ボタン3がSELECT,ボタン4がSTARTになってしまいます。


###クラス
#####js = joy.Joy( id )
n番目のジョイスティックを初期化します。

####プロパティ
`js.id` ジョイスティックのID
`js.name` ジョイスティックの製品名
`js.buttons` ジョイスティックが持つボタン数
`js.axes` ジョイスティックが持つ方向軸の数
`js.hats` ジョイスティックのハットボタンの数

####メソッド
#####js.start( [callback] )
ジョイスティックの監視を開始します。callbackに任意の関数をいれておくと操作があった場合にその関数が呼び出されます。
#####js.stop()
監視を停止します
#####st = js.get_status()
現在のジョイスティックの状態を返します
#####st = js.get_pushed()
前回にジョイスティックの状態を取得してからされたジョイスティックの動作を返します
※get_statusではボタンを押しっぱなしの時は常に押されていると判定しますが、get_pushedは押しっぱなしの場合押した最初の呼び出しのみ押されている判定がされます
#####js.clear_pushed()
`js.get_pushed()`の状態をリセットします

####コールバック関数
#####callback(status)

####状態の見方
ボタンの押下状態は、以下の形で返されます
`st.id` ジョイスティックのid
`st.name` ジョイスティックの名前
`st.button[]` ボタンnの状態 (0 = 押されていない,1 = 押されている)
`st.axes[]` n軸の状態 (0～1.0)
`st.hat[]` ハットスイッチの状態(-1 , 0 , 1)

#注意事項
pygameを使った物とは多分共存できません。
`pygame.event.pump()`が多分フレームの強制スキップなのだと思うのですが、なにやってるかイマイチ理解してないし、pygame内で何が起こってるかわからないので・・・
同じように、２つ以上のジョイスティックを使用してどうなるかも詳しく検証はしていません。