Quanto ao NES (e SNES também principalmente), aqui está uma visão geral básica. Eu não escrevi nenhum jogo NES, mas escrevi um emulador NES (Graybox) e fiz uma boa quantidade de engenharia de revisão de carros antigos.
Quanto à linguagem de programação: sim, era tudo montagem. Programar o NES significava trabalhar diretamente com interrupções de hardware, portas DMA, comutação de bancos etc. Felizmente, programar o 6502 (ou melhor, o 2A03) é bastante fácil [1]:
- Existem poucos registros: A, X e Y principalmente, sendo os dois últimos utilizáveis apenas para indexação e iteração
- o conjunto de instruções é pequeno e principalmente direto
- pouca memória: a RAM principal é de 2 KB, com uma extensão opcional de 8 KB suportada por bateria. Desses 2 KB, 256 bytes são reservados para a pilha e a página 0 (os primeiros 256 bytes) foi onde você deseja armazenar seus ponteiros e valores mais usados devido a alguns modos de endereçamento especiais
Essas três coisas juntas criam um ambiente fácil de memorizar enquanto você trabalha com ele. Sim, você mesmo gerencia toda a memória, mas isso significava essencialmente que você cria um mapa completo de onde tudo vai adiante e esse mapa não é muito grande, porque você só precisa se preocupar com 2K, para que possa traçar isso em um pedaço de papel gráfico. Você precisava planejar um pouco mais e atribuir variáveis e constantes estaticamente aos locais de RAM e ROM (no cartucho).
Fica um pouco mais complicado quando os dados do seu cartucho ultrapassam os limites endereçáveis da CPU. São 64 KB, dos quais os 32 KB inferiores são definidos em pedra e mapeados para todos os tipos de portas de hardware e RAM. É aqui que a troca de banco entra em ação, o que significa mapear uma seção da ROM para (parte do) espaço de endereço de 32 KB mais alto.
Isso pode ser usado da maneira que o programador desejar, mas um exemplo de uso pode estar em um jogo com 3 níveis, com todos os dados de nível, metadados e código de cada nível, amontoados em áreas de 8KB de memória separadas no cartucho. O nível pode ter retornos de chamada para, por exemplo, inicialização, atualização por quadro, etc. "Carregando" o nível significaria mapear esse pedaço de 8 KB de memória em, por exemplo, 0xC000. Em seguida, você pode especificar que a rotina init esteja sempre em 0xC000, a rotina de atualização de quadros esteja em 0xC200 e os dados de nível iniciem em 0xC800. O código principal do jogo, alojado em outro pedaço de memória, controla as alterações de nível simplesmente trocando o pedaço direito e saltando para os endereços absolutos 0xC000 e 0xC200 nos horários apropriados.
Dados gráficos errados: os dados dos blocos do NES são mapas de 2 bits e 8x8 pixels. Para o segundo plano, eles são combinados com uma camada de 2 bits e 1/4 de resolução. Esses valores de 4 bits foram então indexados em uma paleta de 16 entradas, e acredito que 53 cores exclusivas efetivas estejam disponíveis. Os sprites também usaram os dados de pixel de 2 bits e cada sprite especificou seu próprio índice de grupo de 2 bits, formando novamente um índice pal de 4 bits. A imagem BG na tela é uma matriz de 32x30 de números de índice de bloco.
Essencialmente, com uma tonelada de repetição e índices em índices, você pode manter os dados muito pequenos. Os dados de nível geralmente eram armazenados como barras verticais de índices de bloco e, como essas barras verticais também eram reutilizadas, elas também eram indexadas e armazenadas apenas uma vez no cartucho. Técnicas simples de compactação de dados funcionam de maneira semelhante. Isso permitiu que o Mario 1 tivesse 32 KB de dados (com espaço de sobra) e 8 KB de dados de bitmap.
Quanto aos ambientes de desenvolvimento, vi algumas fotos em que as pessoas trabalhavam em computadores antigos comprovadamente conectados a gravadores EEPROM para o trabalho. A depuração assistida por ferramentas não era realmente uma possibilidade até depois da idade do SNES [2]. Esta é a principal razão pela qual muitos jogos antigos possuem bugs "óbvios" e por que coisas como Gameshark podem fazer o que fazem; a saúde do jogador sempre estaria no local X da memória, para que você possa forçar 100 a qualquer momento.
Se você achar essas coisas interessantes, encorajo você a consultar, por exemplo, http://wiki.nesdev.com/w/index.php/Nesdev_Wiki.
Existem muitos cursos de programação para o NES também online.
Espero que essa visão simplificada tenha dado algumas dicas sobre o desenvolvimento de jogos da era dos anos 80.
[1] Relativamente falando. Também sou tendencioso quando escrevi o Graybox em cerca de 85% do conjunto PowerPC. [2] Veja a elaboração do artigo FF6: http://www.edge-online.com/features/the-making-of-final-fantasy-vi/