Продолжаю серию статей (раз, два) про "тетрисы" и используемые в них микроконтроллеры. В предыдущих частях был описан 4-битный контроллер Holtek, вариации которого использовались в Brick Game и многих других портативных электронных играх, выпускавшихся в 90-х годах.
С тех пор я декапсулировал и отснял под микроскопом больше десятка кристаллов этого семейства из различных игр. Среди них были как привычные Brick Game и разные электронные брелки, так и игры от именитых производителей, например из серий Nintendo Mini Classics и Bandai Mame. Для девяти из них удалось прочитать ПЗУ и добавить в эмулятор из предыдущей статьи.

Естественно, мне встречались микроконтроллеры не только Holtek, но и других производителей. Их я тоже изучал и по возможности дампил и эмулировал. Об одном таком я расскажу подробнее в этой статье.
Apollo 126 in 1 English Talking (B0202)

Мой экземпляр выпущен в апреле 1995 года. Корпус чёрного цвета, обычный для линейки Apollo. Расположение сегментов на дисплее точно такое же, как на многих других Brick Game, в том числе E-23 из предыдущей статьи. Но набор игр немного нестандартный: кроме обычных тетрисов и разных гонок-стрелялок, есть, например, подобие Frogger и слот-машина. Ещё одна отличительная особенность – голосовые комментарии, сопровождающие игры.
Под каплей компаунда обнаружился микроконтроллер Sunplus с маркировкой PA2071-160:

Первым делом, необходимо было определить название микроконтроллера или хотя бы его семейство, по которому можно будет попробовать найти документацию. Для поиска у нас есть сразу несколько параметров, определяемых по фотографии: производитель Sunplus, ПЗУ 20 КБ, ОЗУ 192 Б, ЖК драйвер на 40 сегментов и 8 общих выводов, 8-битное ядро 6502. Плюс к этому так как игра имела голосовое сопровождение, у искомого контроллера должны быть соответствующие возможности. С этой информацией идём на archive.org и исследуем самые ранние бэкапы sunplus.org.tw, находим там такие красивые таблицы и по ним подбираем максимально подходящий SPL02B. Можно заметить несоответствие размера ПЗУ: заявлено только 19.5 КБ, а у нас вроде как 20, причина расхождения будет понятна позже.
Документация на SPL02B, к счастью, доступна, хотя и очень поверхностна, что обычно для специализированных микроконтроллеров того времени.

Есть ещё некоторая нестыковка по контактным площадкам: на нашем кристалле присутствуют две лишние вокруг VDD2 и VDD3, которых нет на схеме из даташита (рисунок слева). Не знаю, с чем это связано, то ли не совсем верно определен микроконтроллер и это не SPL02B, а, допустим, SPL02A (документации на который найти не удалось); то ли площадки изначально не предназначены для разварки, на что намекает отсутствие на них следа от зонда тестирования.
Несмотря на эти небольшие нестыковки, в целом микроконтроллер установлен и можно оценить его параметры по официальной документации: 8-битное ядро (урезанная вариация 6502) с частотой до 1.5 МГц, 19.5 КБ ПЗУ, 192 Б ОЗУ, возможность подключения 32 кГц часового кварца, а звуковая часть включает два канала тонов, канал шума и речевой канал. Очевидно, характеристики заметно выше, чем у Holtek из предыдущей статьи.
Рассмотрим каждый параметр подробнее.
6502

На фото слева ядро SPL02, а справа оригинальный MOS 6502D. Видно, что топологии совершенно разные, только структура остаётся той же: дешифратор инструкций сверху, восьмиразрядное АЛУ снизу и управляющая логика между ними.
ПЗУ

Цифрами слева обозначены участки, соответствующие битам каждого разряда, а цветными прямоугольниками размечены области основной памяти программ и четырёх дополнительных банков. Если перевернуть карту распределения адресного пространства из даташита, то порядок блоков ПЗУ будет соответствовать тому, что мы видим на фото:

Изучая структуру ПЗУ, можно понять причину несоответствия фактического объема памяти заявленному в документации: первые 512 байт, отведённые под ОЗУ и регистры, физически присутствуют на кристалле, но не адресуются. Если увеличить участок ПЗУ с каждым нулевым битом, занимающий адреса $0000 - $07FF, то видно, что первые 512 байт просто заполнены нулями (та же картина наблюдается и для остальных разрядов):


Слева показан увеличенный фрагмент, обведённый красным, где подписано значение ячеек памяти. Видно, что разрешение снимка оставляет желать лучшего, но у меня просто нет достаточно качественных объективов для съёмки больших областей такого плотного техпроцесса. Хотя даже так, биты вполне читаемы, а поигравшись с фокусом и апертурой, у меня получилось сделать фотографии подходящие для автоматического распознавания по разности контраста, с приемлемым количеством ошибок, в тысячные доли процента. Но даже тысячные процента от 163840 бит – это сотни ошибок, раскиданных по изображению с разрешением около 6500x14000, которые потом я искал и исправлял вручную, просматривая фотографию бит за битом. Долгая и нудная работа.
Фото всего ПЗУ в двух вариантах съёмки можно найти здесь.
ОЗУ

Размер ОЗУ – 192 байта: первые 48 байт занимает видеопамять, где каждые 6 байт отводятся под один общий вывод, а каждый бит в этом блоке управляет одним из 48 сегментов. Так как микроконтроллер поддерживает только 40 сегментов, каждый шестой байт не используется. Остальные 144 байта доступны для общего назначения, и организации стека.
Регистры ввода-вывода и управления
Как и у большинства микроконтроллеров, SPL02 имеет специальные регистры, управляющие периферией и режимами работы. Под них отведено 64 байта по адресам $00C0 - $00FF, хотя фактически используется только 14 регистров. И здесь возникает одна из основных сложностей для эмуляции этого микроконтроллера, потому что в даташите регистры описаны в общих чертах, для большинства даже не указаны адреса. Частично эту проблему помогла решить среда разработки G+ IDE for 6502, в которой есть заголовочные файлы для этого микроконтроллера с более детальным описанием регистров:
;----------------------------------------------
; I/O configuration for hardware system
;----------------------------------------------
P_C0H_IO_Ctrl EQU 0C0H
;?? duty
; ? cpu clock
; ? Frosc
; ? port b 1:0
; ? port a 7:4
; ? port a 3:0
P_C1H_PortA_DataPort EQU 0C1H
P_C3H_PortB_DataPort EQU 0C3H
P_C4H_Freq_ToneA EQU 0C4H
P_C6H_Freq_Vol_ToneA EQU 0C6H
P_C8H_Freq_ToneB EQU 0C8H
P_CAH_Freq_Vol_ToneB EQU 0CAH
P_CCH_Noise_Type EQU 0CCH
P_CEH_Noise_Vol EQU 0CEH
P_D0H_Standby_Normal_Config EQU 0D0h
P_D2H_INT_Config EQU 0D2h
P_D4H_Speech_Mode EQU 0D4h
P_D5H_Speech_Data_Port EQU 0D5h
P_D7H_Bank_Select_Port EQU 0D7h
А некоторые моменты получилось выяснить уже на практике – запуская код на реальном железе. Об этой возможности я расскажу чуть дальше.
Звук

Отличительной особенностью этого микроконтроллера является наличие 7-битного ЦАП через который выводится звук с двух тональных каналов, одного канала шума и речевого канала, который непосредственно управляет ЦАПом через регистр Speech. Благодаря последнему, есть возможность программного воспроизведения 7-битного звука – именно это позволяет "тетрису" Apollo комментировать игру разными короткими, неразборчивыми фразами.
На самом деле, даже семью битами при достаточной частоте дискретизации можно воспроизводить относительно качественный звук, но из-за ограниченного размера ПЗУ, разработчики не могли позволить себе в полной мере раскрыть потенциал микроконтроллера. Я немного изучил код воспроизведения и выяснил, что фразы хранятся в 4-битном PCM, по два сэмпла на байт, которые табличной подстановкой растягиваются на весь 7-битный диапазон и воспроизводятся с частотой около 6 кГц. Даже при таких ограничениях, полусекундное воспроизведение слова "Apollo" занимает 1406 байт.

Эмуляция
Прошлую статью я закончил упоминанием эмулятора BrickEmuPy, который я тогда написал под микроконтроллеры Holtek и конкретно для E-23. Изначально я думал на этом и остановиться, но тема эмуляции меня так увлекла, что я стал покупать другие "тетрисы" и вообще все игры с сегментными ЖК экранами 80-90-х годов, декапсулировать их для получения прошивок и добавлять в свой эмулятор.
Естественно, не все купленные игры работали на Holtek'ах, но если у микроконтроллеров было ПЗУ, которое я мог оптически прочитать, я так же добавлял их в BrickEmuPy. SPL02 стал уже 11-м семейством микроконтроллеров, поддержка которых добавлена в BrickEmuPy, а общее количество игр сейчас приближается к 50.
Видео, демонстрирующее эмуляцию Apollo 126 in 1:
Запуск внешнего кода
Отлаживая эмулятор B0202 на полученном образе ПЗУ, я наткнулся на интересный участок кода, вызываемый внешним прерыванием:
2FC0: sei ;78
2FC1: lda #$CB ;A9CB
2FC3: sta $C0 ;85C0
2FC5: lda #$00 ;A900
2FC7: sta $C6 ;85C6
2FC9: sta $CA ;85CA
2FCB: sta $CE ;85CE
2FCD: lda #$01 ;A901
2FCF: sta $C3 ;85C3
2FD1: lda #$00 ;A900
2FD3: sta $C3 ;85C3
2FD5: ldx #$00 ;A200
2FD7: lda $C1 ;A5C1
2FD9: sta $38, X ;9538
2FDB: lda #$02 ;A902
2FDD: sta $C3 ;85C3
2FDF: inx ;E8
2FE0: lda #$00 ;A900
2FE2: sta $C3 ;85C3
2FE4: cpx #$78 ;E078
2FE6: bne 2FD7 ;D0EF
2FE8: lda #$01 ;A901
2FEA: sta $C3 ;85C3
2FEC: jmp 0038 ;4C3800
;когда уже Хабр начнет подерживать разные ассемблерные нотации :(
Эта подпрограмма читает 120 байт из порта A и кладет их в ОЗУ начиная с $0038, тактируя передачу вторым разрядом порта B, после чего переходит по этому адресу. Если какие-либо данные с порта поступили, микроконтроллер естественно начнет их выполнять, а если нет и там окажется 0, что соответствует инструкции BRK, вызывающей внешнее прерывание, то история повторится пока что-то все же не будет получено.
Такой вот отладочный режим, позволяющий выполнять внешний код уже после изготовления кристаллов с масочным ПЗУ на заводе.
Мягко говоря, меня заинтересовала возможность запуска своего кода на "тетрисе". Первой мыслью было купить такой же B0202 и попробовать вызвать этот отладочный режим, но у меня лежал и ждал своего часа ещё один Apollo – 18 в 1 B0302. Распиновка у него оказалась очень похожа на B0202, поэтому я предположил, что в нем используется тот же или похожий микроконтроллер и стал экспериментировать на нем.

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

Первым делом, я попробовал передать инструкции просто нажимая кнопки на "тетрисе". Конечно, это будет одна команда повторённая 120 раз, но для проверки теории сгодится. И да – это сработало: микроконтроллер предсказуемо прерывал передачу на то количество тактов, которые соответствовали количеству переданных инструкций. Уже на этом этапе я столкнулся со второй сложностью: от порта A на кнопки выведено только 7 младших разрядов, так что все инструкции, у которых задействован старший бит, напрямую передать не получится. Как я это обходил расскажу чуть позднее.
Следующая задача, которую надо было решить – это сделать какой-нибудь источник данных, который бы по тактовому сигналу с порта B нажимал кнопки за нас передавал данные на порт A. Для этого я использовал Raspberry Pi Pico 2, которая будет принимать данные с ПК через встроенный последовательный порт и после того, как тактовый импульс с порта B будет стабильным некоторое время, передавать их на микроконтроллер Apollo.

Всё готово для передачи осмысленного набора инструкций, но сперва надо разобраться с проблемой отсутствующего старшего бита. Сначала, конечно, я попробовал просто не использовать такие инструкции. И здесь меня поджидала очередная проблема: программы выполнялись некорректно если использовались инструкции с абсолютным адресом. Оказалось, что, в отличие от B0202, отладочный режим B0302 кладёт полученные данные не с $0038, а с $0000. Разобравшись с этим , я наконец смог запустить свой код. Не помню точно, какой именно, но, допустим, он мог выглядеть так:
0000: sec
loop:
0001: rol $00
0003: jmp loop

Результат выполнения на GIFке слева. Программа просто бесконечно сдвигает нулевой байт ОЗУ и сегменты адресованные на него начинают мерцать. Статические сегменты – это кусочки самой программы, поскольку она располагается в области ОЗУ, отведённой под видеопамять. Обратите внимание на способ запуска: я наклеил медную ленту одним концом на дорожки около микросхемы, а второй на корпус Apollo, таким образом нет необходимости лезть внутрь чтобы вызвать прерывание.
Безусловно, имея только половину инструкций, много не напишешь. Надо добавлять какой-то исправляющий код перед выполнением основного блока программы и лучшее, что мне пришло в голову: передавать каждый байт, использующий старший бит сдвинутым вправо, а в исправляющей части сдвигать его обратно через флаг переноса, предварительно установив или сбросив его соответственно младшему разряду. Моё, возможно, путаное объяснение лучше проиллюстрировать кодом:
;исправляющий код
0000: sec
0001: rol $03
;исправляемый код
0003: .byte $54, $00
;после выполнения первых двух инструкций, "54 00" превратится в "A9 00"
;что соответсвует недоступной ранее инструкции lda #0
У этого подхода есть существенный недостаток – на одно исправление тратится 2-3 байта, что довольно расточительно, когда у нас их всего 120. Да и размер исправляющего кода будет непостоянным, что не очень удобно. Поэтому, в итоге я остановился на таком варианте подготовки кода перед отправкой:
dataSize = 74
;исправляем основной код методом описанным выше
sec
rol $17
rol $0E
rol $10
rol $15
sec
rol $16
rol $1B
;основной код исправляющий data
ldx #dataSize-1
mainLoop:
inc loBitPtr+1
clc
loop8:
loBitPtr:
ror loBit-1
beq mainLoop
lda data, X
rol
pha
dex
bmi start
jmp loop8
loBit:
;здесь лежат младшие биты
* = 120-dataSize
data:
;здесь сдвинутые вправо байты
* = 128-dataSize
start:
Таким способом получается передать 74 полных байта и положить их начиная с $0036. Наверняка можно придумать и ещё что-то более оптимальное (например передать сначала отдельный загрузчик), но я решил на этом остановиться.
На Python я написал скрипт, который принимает до 74 байт машинного кода, объединяет с программой выше и передает готовые 120 байт на Pico. Весь процесс теперь автоматизирован: остаётся только коснуться частотозадающего резистора на Apollo и Pico передаст программу, которая тут же начнет выполняться.
Считывание прошивки
Первым делом была написана программа передающая прошивку B0302 по порту B:
ldx #$00
lda #$00
bankLoop: ;обходим банки ПЗУ
sta $D7 ;записывая номер текущего в регистр выбора банка D7
byteLoop:
lda (romPtr, X) ;получаем очередной байт ПЗУ
rol
ldx #$08
bitLoop: ;сдвигаем этот байт бит за битом
and #$FE
sta $C3 ;и передаем его через 1-ый разряд порта B
ora #$01
sta $C3 ;а по 0-ому тактируем передачу
ror
dex
bne bitLoop
inc romPtrL ;инкрементируем 16-битный адрес
bne byteLoop
inc romPtrH
lda romPtrH
cmp #$20
bne byteLoop ;пока не доходим до конца банка
lda #$10 ;все банки начинаются с $1000
sta romPtrH
inc bank ;переходим к следующему банку
lda bank
cmp #$04
bne bankLoop ;пока не доходим до последнего возможного
brk ;после завершения передачи начинаем сначала
romPtr:
romPtrL:
.byte $00
romPtrH:
.byte $00
bank:
.byte $00
Переданные через порт B данные я считывал логическим анализатором. И никаких сложностей с декапсуляцией, фотографированием и распознаванием битов!
Оказалось, что у B0302 только два банка ПЗУ (на остальных данные повторяются) т.е. всего 11.5 КБ ПЗУ. Если мы обратимся к той же таблице с параметрами микроконтроллеров, то такой размер есть только у SPL03. Выходит, в этих двух Apollo стоят немного разные контроллеры и можно предположить, что цифры в кодах игр B0202 и B0302 указывают на их индекс. Тут же становится понятно, почему данные кладутся начиная с $0000 а не с $0038 – размер ОЗУ здесь ещё меньше, чем у B0202, только 128 байт. Ещё одно неприятное ограничение: вместо 7-битного ЦАП, у SPL03 только 6-битный ШИМ, причем он раскидан на два вывода, а схема Apollo использует только один, то есть фактически, для вывода звука можно использовать только 5 бит.
Bad Apollo
Изначально я планировал написать какую-нибудь простенькую игру, но после того, как стало понятно, что на все про все доступно даже не 192 байта, а только 128, эту идею пришлось отбросить. Тогда решил написать проигрыватель "Bad Apple!!" – заведомо эффектное демо для запуска на "тетрисе", при этом, если передавать несжатые данные, достаточно простое для реализации.
Видео и аудио будут храниться на Pico 2, который выступает в роли внешнего ПЗУ с последовательным доступом, а на самом Apollo программа будет запрашивать и получать данные синхронно частоте кадров, которая равна 1/16384 от частоты работы микроконтроллера, то есть примерно 42 кадра в секунду. При этом, большую часть данных занимает аудиопоток в виде 5-битного PCM с частотой около 22 кГц.
Поэкспериментировав некоторое время, я остановился на таком формате данных:
Каждый передаваемый байт (без старшего бита, как вы помните) содержит 5-битный звуковой сэмпл, и 1 бит под состояние одного сегмента. На обработку одного байта я выделил 32 цикла ядра SPL03, за это время необходимо запросить байт, получить его, закинуть сэмпл в голосовой порт и включить/выключить нужный сегмент. Всего видеопамять на SPL03 использует 32 байта, но они раскиданы на 48 байтах, которые я для удобства округлил до 64. В итоге, на каждый кадр уйдет 64 × 8 × 32, то есть 16378 циклов, так что обновляться дисплей будет синхронно с частотой кадров ЖК дисплея – что и требовалось. При этом, частота дискретизации звука получится равной 1/32 от частоты микроконтроллера, что для B0302 будет примерно соответствовать 21.7 кГц – вполне достаточно, более качественный несжатый звук просто не влезет в 4 МБ флэш Pico 2.
Подготовка данных
С аудиопотоком проблем никаких не возникло: просто открыл аудиодорожку оригинального видео в Audacity и экспортировал в 8-битный сырой PCM с нужной частотой дискретизации.
С видео пришлось повозиться дольше. Для начала, надо было определиться с частотой кадров – в оригинальном видео 30 кадров в секунду, а в B0302, как я уже упоминал, 42. И здесь можно пойти двумя путями: просто дублировать некоторые кадры подгоняя оригинальную частоту к B0302. Или наоборот, отбрасывать до получения 21 к/с – половины нужной частоты, но при этом каждый лишний кадр использовать для имитации дополнительного оттенка серого. Сравнив оба варианта, я остановился на втором.
Аудио и Видео объединяет Python-скрипт, который отбрасывает у аудиопотока лишние 3 младших бита и кладет оставшиеся в 5-1 разряды, а младший разряд устанавливает по состоянию одного из 256 сегментов перебирая их последовательно для кадр за кадром.
Проигрыватель
Данные подготовлены, пор�� перейти к коду, который будет их воспроизводить на Apollo:
* = $36
ldx #$36
txs
jsr clearLCD
lda #$01
sta $D4 ;включаем Speech Play Mode
main:
ldx #00
mainLoop:
lda #$02 ;просим у Pico очередной байт
sta $C3 ;по переднему фронту B1
lda #$00
sta $C3
lda $C1 ;получаем его из порта A
sta $D5 ;и сразу кладем в порт Speech
ror ;сохраняем 0-й бит с состоянием сегмента во флаг переноса
lda segStates
rol ;сдвигаем segStates влево добавляя состояние нового сегмента
bcc byteNotFull ;и одновременно проверяя, получен ли полный байт
sta $00, X ;сохраняем полученный байт в видеопамять
inx
lda #$02 ;дальше дублируем тот же процесс что и выше, но
sta $C3 ;с небольшими изменениями, чтобы исключить задержку
lda #$01 ;связанную с сохранением байта в видеопамять
sta $C3 ;и уложиться ровно в 32 цикла
sta segStates
lda $C1
sta $D5
ror
rol segStates
cpx #64 ;проходим по всем 48 + 16 байтам видеопамяти
beq main ;некритично задевая область кода
jmp mainLoop
byteNotFull:
sta segStates
jmp mainLoop
segStates:
.byte 01
* = $055B
clearLCD:
За счет удобного формата входных данных, код получился компактным, и без проблем помещается в доступные 74 байта, единственная сложность здесь была уложиться ровно в 32 цикла на обработку байта, ни больше ни меньше, иначе частота обновления видеопамяти будет не совпадать с частотой обновления ЖК дисплея, из-за чего появятся неприятные мерцания.
Что в итоге получилось:
Заключение
Конечно, Bad Apollo – это просто демо для привлечения внимания. Главным и самым сложным в описанной работе было получение прошивок ещё двух Brick Game из 90-х, что позволило написать их эмуляторы и сохранить для истории (как бы это пафосно не звучало).
Надеюсь, со временем получится сдампить и другие модели Apollo, которых, похоже, было не меньше десятка. Но, конечно, совсем не обязательно, что на всех них будет так же легко вызвать внешнее прерывание – я, например, пробовал провернуть то же самое на Apollo 12 in 1 и у меня ничего не вышло.
Если кто-то после прочтения этой статьи захочет поэкспериментировать с другими моделями Apollo, делитесь результатами в комментариях или пишите в личку, даже если они будут отрицательными. Со своей стороны, постараюсь ответить на возможные вопросы.
Исходники описанного в статье опубликованы на GitHub
Фотографии и другие материалы по Apollo B0202 опубликованы на archive.org
Комментарии (10)

Swamp_Dok
06.11.2025 08:48Не ожидал, что и в тетрисе 6502. Особенно забавно, что я тоже пишу игры под 6502 для денди) 6502 повсюду.
Это ведь ещё и эпл 2 и коммодор 64, жаль только у всех слишком разная обязка и весь софт пропитан хаками, портировать проблематично.

Azya Автор
06.11.2025 08:48О да, 6502 был очень популярен и до сих пор встречается в подобных игрушках. Nintendo, кстати, кроме Famicom, еще использовала 6502 под своим брендом в шагомере Pocket Pikachu 1998 года и в некоторых Mini classics в нулевых.

zartarn
06.11.2025 08:48Теперь осталось это в fpga повторить, и сделать пинсовместимым) хотя по воспоминания из детства, чаще умирали 'резинки', которыми подключался дисплей.
dlinyj
Большая моя личная благодарность за ваши статьи. Они меня очень вдохновили. Я ковырялся с тетрисами, тоже пытался их завести. Из забавного, пытался сделать эмулятор из новодельных тетрисов. Хотя бы потому, что их валом можно купить дёшево, а старые исчерпаемый ресурс и стоят дорого.
Но в какой-то момент вдохновение покинуло меня в этом вопросе. Какие-то наработки остались, но всё либо выбросил, либо продал.
Azya Автор
Спасибо за теплые слова! В смысле вы хотели поменять железо и запустить на нем эмулятор в корпусе новодела?
dlinyj
Идея была ваш эмулятор перенести в 2040, и дальше уже грузить образы в реальном железе.
Я купил обычный дешёвый тетрис. Сделал ему реверс клавиатуры (ооочень мудрёно сделана). Скрины с видео:
Сделал плату для коннекта дисплея (которая вполне себе работала).
Даже написал свой тетрис, и игрался с этой клавиатуры в консоли. Но потом, что-то вдохновение кончилось
Azya Автор
Классно! Понимаю, тоже копятся старые недоделанные проекты, на которые иссяк запал(
dlinyj
У меня много сил отняло бодание к RP 2040, я хотел сделать многопоточку. Чтобы один поток отвечал за эмуляцию и логику, а второй за отображение и работу с кнопками. Играл и проиграл
Azya Автор
Так обычно и бывает – хочется сделать красиво и основательно, а в итоге устаешь от задачи или появляется новая более безумная идея)