Até agora, todos os dados que nossos programas utilizaram (como a posição das bombas no Campo Minado) eram perdidos quando o programa fechava. Para resolver isso, precisamos de um método de persistência de dados, ou seja, uma forma de salvar informações para que possam ser recuperadas depois. A maneira mais direta de fazer isso é através da manipulação de arquivos de texto. Neste capítulo, aprenderemos a ler e escrever em arquivos para salvar o layout do nosso jogo e o placar de vitórias e derrotas.
Quando um programa precisa ler ou escrever em um arquivo, ele estabelece um "fluxo" de comunicação. Para evitar que outro programa (ou até o próprio usuário) modifique o arquivo ao mesmo tempo e cause erros, o arquivo fica "travado" durante o uso. Por isso, o procedimento padrão é sempre:
Para que nosso programa encontre o arquivo, precisamos dizer a ele onde procurar. Existem duas formas de fazer isso: usando um caminho absoluto ou relativo.
Primeiro, crie um arquivo chamado campo.txt e salve-o na pasta principal do seu projeto (no mesmo local
do arquivo .sln), como mostra a figura:
É o endereço completo do arquivo, começando da raiz do seu HD (como C:\). É específico para cada
computador. Ao usar em uma string em C#, precisamos duplicar as barras invertidas (\\) para que a
linguagem não as confunda com caracteres de comando.
// Exemplo de caminho absoluto em uma string C# string caminho_absoluto = "C:\\Users\\Rafael\\source\\repos\\Capitulo3\\campo.txt";
É o endereço do arquivo tomando como ponto de partida o local onde o programa está sendo executado.
No caso de um projeto C# no Visual Studio, o executável fica numa pasta como bin\Debug\netcoreappX.X.
Para chegar ao nosso arquivo na raiz do projeto, precisamos "subir" alguns níveis de pasta. Usamos ..
para subir um nível.
// Exemplo de caminho relativo para subir 4 níveis e encontrar o arquivo string caminho_relativo = "..\\..\\..\\..\\campo.txt";
Vamos alterar nosso Campo Minado para que ele carregue o mapa e o placar do arquivo campo.txt. O arquivo
precisa ter um formato específico para que o programa consiga entendê-lo: 10 linhas para o campo (com números
separados por vírgula), uma linha em branco, uma linha para vitórias e uma para derrotas.
O código a seguir abre o arquivo, lê os dados e monta o tabuleiro do jogo.
int[,] campo = new int[10, 10];
int[,] jogo = new int[10, 10];
int qtdLinhas = campo.GetLength(0);
int qtdColunas = campo.GetLength(1);
bool problemaArquivo = false;
string caminho_relativo = "..\\..\\..\\..\\campo.txt";
try
{
// 1. Cria um leitor de fluxo para o arquivo especificado
StreamReader sr = new StreamReader(caminho_relativo);
string linha_arq = sr.ReadLine(); // 2. Lê a primeira linha do arquivo
int linha_mtz = 0;
// 3. Continua lendo linha por linha, até o fim do arquivo ou até ler as 10 linhas do campo
while (linha_arq != null && linha_mtz < 10)
{
int coluna_mtz = 0;
// 4. Separa a string da linha pela vírgula (ex: "0,0,1,0...") em várias strings menores ("0", "0", "1", "0"...)
foreach (var numero in linha_arq.Split(','))
{
int num;
// 5. Tenta converter a string para um número inteiro de forma segura
if (int.TryParse(numero, out num))
{
campo[linha_mtz, coluna_mtz] = num; // Armazena na matriz do campo
jogo[linha_mtz, coluna_mtz] = -1; // Inicia a matriz do jogador
coluna_mtz++;
}
}
linha_arq = sr.ReadLine(); // Lê a próxima linha
linha_mtz++;
}
// 6. Fecha o fluxo para liberar o arquivo
sr.Close();
}
catch (Exception e)
{
// 7. Se qualquer erro acontecer no bloco 'try', o código pula para cá
Console.WriteLine("Ocorreu um problema na leitura do arquivo!");
problemaArquivo = true;
}
try-catch: Operações com arquivos são arriscadas (o arquivo pode não existir, estar corrompido,
etc.). O bloco try tenta executar o código de leitura. Se qualquer erro (uma "exceção") ocorrer, o
programa não trava, ele pula para o bloco catch e nos avisa do problema.StreamReader: É a classe do C# responsável por ler arquivos de texto.sr.ReadLine(): Lê uma linha inteira do arquivo e avança o "cursor" para a próxima.linha_arq.Split(','): Um método muito útil que quebra uma string em um vetor de strings, usando o
caractere que você definir como separador (neste caso, a vírgula).int.TryParse(): Uma forma segura de converter texto em número. Diferente de
int.Parse(), ele não causa um erro se a conversão falhar; em vez disso, ele retorna
false.
sr.Close(): Passo fundamental! Sempre feche o arquivo após o uso para que outros processos possam
acessá-lo.O restante do código do jogo agora é colocado dentro de uma condição para só ser executado se o arquivo for lido com sucesso.
if (!problemaArquivo)
{
bool fimJogo = false;
bool vitoria = false;
do
{
// Imprime o tabuleiro do jogador a cada rodada
for (int l = 0; l < qtdLinhas; l++)
{
for (int c = 0; c < qtdColunas; c++)
{
Console.Write(string.Format("{0} ", jogo[l, c]));
}
Console.Write(Environment.NewLine + Environment.NewLine);
}
// Pede a jogada para o usuário
Console.Write("Selecione uma linha [1-10]: ");
int linha = Convert.ToInt32(Console.ReadLine()) - 1;
Console.Write("Selecione uma coluna [1-10]: ");
int coluna = Convert.ToInt32(Console.ReadLine()) - 1;
// Verifica o resultado da jogada
switch (campo[linha, coluna])
{
case 0:
jogo[linha, coluna] = 0;
Console.Write("Continue tentando.\n\n");
break;
case 1:
jogo[linha, coluna] = 1;
Console.Write("BOOOM. Você perdeu.\n\n");
fimJogo = true;
break;
default: // Caso encontre a bandeira (valor 2)
jogo[linha, coluna] = 2;
Console.Write("Parabéns. Você ganhou!\n\n");
fimJogo = true;
vitoria = true; // Atualiza a flag de vitória
break;
}
} while (!fimJogo);
}
Após o fim de cada partida, vamos atualizar o placar de vitórias ou derrotas no arquivo. A estratégia será: ler todo o conteúdo do arquivo para a memória, modificar apenas a linha que queremos e, em seguida, reescrever o arquivo inteiro com o conteúdo atualizado.
Este código deve ser inserido dentro do if (!problemaArquivo), logo após o final do loop
do-while do jogo.
try
{
// 1. Lê todas as linhas do arquivo e guarda em um vetor de strings
string[] arquivo = File.ReadAllLines(caminho_relativo);
// Pega as linhas de vitória e derrota (penúltima e última)
string msgVitorias = arquivo[arquivo.Length - 2];
string msgDerrotas = arquivo[arquivo.Length - 1];
// 2. Abre um fluxo de ESCRITA. IMPORTANTE: Isso apaga o conteúdo atual do arquivo!
StreamWriter sw = new StreamWriter(caminho_relativo);
int contagem;
int linha_sobrescrever;
string texto;
if (vitoria)
{
// Pega o número depois de "Vitórias:"
int.TryParse(msgVitorias.Split(':')[1], out contagem);
linha_sobrescrever = 11; // A contagem de vitórias está na 12ª linha (índice 11)
texto = "Vitórias:";
}
else
{
// Pega o número depois de "Derrotas:"
int.TryParse(msgDerrotas.Split(':')[1], out contagem);
linha_sobrescrever = 12; // A contagem de derrotas está na 13ª linha (índice 12)
texto = "Derrotas:";
}
contagem++; // 3. Incrementa o placar
// 4. Percorre o vetor que continha as linhas originais
for (int i = 0; i < arquivo.Length; i++)
{
// Se for a linha que queremos mudar...
if (i == linha_sobrescrever)
{
sw.WriteLine(texto + contagem); // Escreve a nova linha com o placar atualizado
}
else
{
sw.WriteLine(arquivo[i]); // Senão, apenas reescreve a linha original
}
}
// 5. Fecha o fluxo para salvar as alterações no arquivo
sw.Close();
}
catch (Exception e)
{
Console.WriteLine("Ocorreu um problema na escrita do arquivo!");
}
File.ReadAllLines(): Um atalho útil para ler um arquivo inteiro e já separá-lo em um vetor de
strings.StreamWriter: A classe para escrever em arquivos de texto. Criá-la com
new StreamWriter(caminho) prepara o arquivo para ser completamente sobrescrito.
sw.WriteLine(): Escreve uma string no arquivo e automaticamente adiciona uma quebra de linha no
final.Veja o seguinte conteúdo para melhor entendimento: link
A manipulação de arquivos é um recurso poderoso e relativamente simples para dar "memória" aos seus programas. Com as
classes StreamReader e StreamWriter, e o uso correto de tratamento de exceções com
try-catch, você pode salvar e carregar configurações, placares, progresso de jogo e muito mais,
tornando suas aplicações mais robustas e dinâmicas.