1
0
mirror of https://github.com/MADTeacher/go_basics.git synced 2025-11-23 21:34:47 +02:00

add new project step and small fix

This commit is contained in:
Stanislav Chernyshev
2025-06-16 11:31:09 +03:00
parent e606a539b9
commit 0029a1d728
68 changed files with 3215 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
package game
type IGameLoader interface {
LoadGame(path string) (*Game, error)
}
type IGameSaver interface {
SaveGame(path string, game *Game) error
}
type IGameStorage interface {
IGameLoader
IGameSaver
}

View File

@@ -0,0 +1,50 @@
package game
import (
"encoding/json"
"os"
"strings"
)
func NewJsonGameStorage() IGameStorage {
return &JsonGameStorage{}
}
type JsonGameStorage struct{}
func (j *JsonGameStorage) LoadGame(path string) (*Game, error) {
if !strings.HasSuffix(path, ".json") {
path += ".json"
}
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
decoder := json.NewDecoder(file)
var game Game
err = decoder.Decode(&game)
if err != nil {
return nil, err
}
return &game, nil
}
func (j *JsonGameStorage) SaveGame(path string, game *Game) error {
if !strings.HasSuffix(path, ".json") {
path += ".json"
}
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
encoder := json.NewEncoder(file)
err = encoder.Encode(game)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,79 @@
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"tic-tac-toe/game"
)
func main() {
reader := bufio.NewReader(os.Stdin)
storage := game.NewJsonGameStorage()
boardSize := 0
for {
fmt.Println("1 - load game")
fmt.Println("2 - new game")
fmt.Println("q - quit")
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
switch input {
case "1":
var loadedGame *game.Game
var err error
for {
fmt.Println("Enter file name: ")
fileName, _ := reader.ReadString('\n')
fileName = strings.TrimSpace(fileName)
loadedGame, err = storage.LoadGame(fileName)
if err != nil {
fmt.Println("Error loading game.")
continue
}
break
}
// Для полного восстановления экземпляра игры
// присваиваем не дессериализуемым полям структуры
// необходимые значения
loadedGame.Reader = reader
loadedGame.Saver = storage.(game.IGameSaver)
loadedGame.Play() // Запускаем игру
case "2":
for {
fmt.Print("Enter the size of the board (3-9): ")
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("Error reading input.")
continue
}
input = strings.TrimSpace(input)
boardSize, err = strconv.Atoi(input)
if err != nil {
// Использовать предыдущий размер по умолчанию
boardSize = game.BoardDefaultSize
}
if boardSize < game.BoardMinSize ||
boardSize > game.BoardMaxSize {
fmt.Println("Invalid board size.")
} else {
break
}
}
board := game.NewBoard(boardSize)
player := game.NewPlayer()
game := game.NewGame(*board, *player, reader,
storage.(game.IGameSaver))
game.Play()
case "q":
return
default:
fmt.Println("Invalid input. Please try again.")
return
}
}
}

View File

@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}", // <- ставим запятую
"console": "integratedTerminal"
}
]
}

View File

@@ -0,0 +1,113 @@
package board
import (
"fmt"
)
const (
BoardDefaultSize int = 3
BoardMinSize int = 3
BoardMaxSize int = 9
)
type Board struct {
Board [][]BoardField `json:"board"`
Size int `json:"size"`
}
func NewBoard(size int) *Board {
board := make([][]BoardField, size)
for i := range board {
board[i] = make([]BoardField, size)
}
return &Board{Board: board, Size: size}
}
// Отображение игрового поля
func (b *Board) PrintBoard() {
fmt.Print(" ")
for i := range b.Size {
fmt.Printf("%d ", i+1)
}
fmt.Println()
for i := range b.Size {
fmt.Printf("%d ", i+1)
for j := range b.Size {
switch b.Board[i][j] {
case Empty:
fmt.Print(". ")
case Cross:
fmt.Print("X ")
case Nought:
fmt.Print("O ")
}
}
fmt.Println()
}
}
// Проверка возможности и выполнения хода
func (b *Board) makeMove(x, y int) bool {
return b.Board[x][y] == Empty
}
func (b *Board) SetSymbol(x, y int, player BoardField) bool {
if b.makeMove(x, y) {
b.Board[x][y] = player
return true
}
return false
}
// Проверка выигрыша
func (b *Board) CheckWin(player BoardField) bool {
// Проверка строк и столбцов
for i := range b.Size {
rowWin, colWin := true, true
for j := range b.Size {
if b.Board[i][j] != player {
rowWin = false
}
if b.Board[j][i] != player {
colWin = false
}
}
if rowWin || colWin {
return true
}
}
// Главная диагональ
mainDiag := true
for i := range b.Size {
if b.Board[i][i] != player {
mainDiag = false
break
}
}
if mainDiag {
return true
}
// Побочная диагональ
antiDiag := true
for i := range b.Size {
if b.Board[i][b.Size-i-1] != player {
antiDiag = false
break
}
}
return antiDiag
}
// Проверка на ничью
func (b *Board) CheckDraw() bool {
for i := range b.Size {
for j := range b.Size {
if b.Board[i][j] == Empty {
return false
}
}
}
return true
}

View File

@@ -0,0 +1,10 @@
package board
type BoardField int
// фигуры в клетке поля
const (
Empty BoardField = iota
Cross
Nought
)

View File

@@ -0,0 +1,79 @@
package game
import (
"bufio"
b "tic-tac-toe/board"
p "tic-tac-toe/player"
s "tic-tac-toe/storage"
)
// Game представляет состояние игры "Крестики-нолики"
type Game struct {
Board *b.Board `json:"board"`
Player p.IPlayer `json:"player"`
Player2 p.IPlayer `json:"-"` // Не сериализуется напрямую
CurrentPlayer p.IPlayer `json:"-"` // Не сериализуется напрямую
Reader *bufio.Reader `json:"-"`
State GameState `json:"state"`
Saver s.IGameSaver `json:"-"`
// Режим игры (PvP или PvC)
Mode GameMode `json:"mode"`
// Уровень сложности компьютера (только для PvC)
Difficulty p.Difficulty `json:"difficulty,omitempty"`
// Флаг для определения текущего игрока
IsCurrentFirst bool `json:"is_current_first"`
}
// NewGame создает новую игру
func NewGame(board b.Board, reader *bufio.Reader, saver s.IGameSaver,
mode GameMode, difficulty p.Difficulty) *Game {
// Создаем первого игрока (всегда человек на X)
player1 := p.NewHumanPlayer(b.Cross, reader)
var player2 p.IPlayer
if mode == PlayerVsPlayer {
// Для режима игрок против игрока создаем второго человека-игрока
player2 = p.NewHumanPlayer(b.Nought, reader)
} else {
// Для режима игрок против компьютера создаем компьютерного игрока
player2 = p.NewComputerPlayer(b.Nought, difficulty)
}
return &Game{
Board: &board,
Player: player1,
Player2: player2,
CurrentPlayer: player1,
Reader: reader,
State: playing,
Saver: saver,
Mode: mode,
Difficulty: difficulty,
IsCurrentFirst: true,
}
}
// switchCurrentPlayer переключает активного игрока
func (g *Game) switchCurrentPlayer() {
if g.CurrentPlayer == g.Player {
g.CurrentPlayer = g.Player2
} else {
g.CurrentPlayer = g.Player
}
}
// updateState обновляет состояние игры на основе текущей доски
func (g *Game) updateState() {
if g.Board.CheckWin(g.CurrentPlayer.GetFigure()) {
if g.CurrentPlayer.GetFigure() == b.Cross {
g.State = crossWin
} else {
g.State = noughtWin
}
return
}
if g.Board.CheckDraw() {
g.State = draw
}
}

View File

@@ -0,0 +1,127 @@
package game
import (
"fmt"
"strings"
b "tic-tac-toe/board"
p "tic-tac-toe/player"
)
// Play запускает игровой цикл
func (g *Game) Play() bool {
fmt.Println("For saving the game enter: save filename")
fmt.Println("For exiting the game enter : q")
fmt.Println("For making a move enter: row col")
for g.State == playing {
g.Board.PrintBoard()
// Определяем, кто делает ход: человек или компьютер
if g.Mode == PlayerVsComputer && g.CurrentPlayer == g.Player2 {
// Если ход компьютера, просто вызываем его MakeMove
fmt.Println("Computer is making a move...")
row, col, _ := g.CurrentPlayer.MakeMove(g.Board)
// Применяем ход компьютера к доске
g.Board.SetSymbol(row, col, g.CurrentPlayer.GetFigure())
} else {
// Если ход человека, запрашиваем ввод
figure := g.CurrentPlayer.GetFigure()
if figure == b.Cross {
fmt.Print("X move: ")
} else {
fmt.Print("O move: ")
}
// Читаем ввод пользователя
input, _ := g.Reader.ReadString('\n')
input = strings.TrimSpace(input)
// Проверка выхода из игры
if input == "q" {
g.State = quit
break
}
// Проверка и выполнение сохранения игры
if g.saveCheck(input) {
continue
}
// Получаем ход человека-игрока через парсинг ввода
hPlayer, ok := g.CurrentPlayer.(*p.HumanPlayer)
if !ok {
fmt.Println("Invalide data. Please try again!")
continue
}
// Парсим ввод и получаем координаты хода
row, col, validMove := hPlayer.ParseMove(input, g.Board)
if !validMove {
fmt.Println("Invalide data. Please try again!")
continue
}
// Устанавливаем символ на доску
if !g.Board.SetSymbol(row, col, hPlayer.GetFigure()) {
fmt.Println("This cell is already occupied!")
continue
}
}
// Обновляем состояние игры
g.updateState()
// Если игра продолжается, меняем игрока
if g.State == playing {
g.switchCurrentPlayer()
}
}
// Печатаем итоговую доску и результат
g.Board.PrintBoard()
fmt.Println()
switch g.State {
case crossWin:
fmt.Println("X wins!")
case noughtWin:
fmt.Println("O wins!")
case draw:
fmt.Println("It's a draw!")
}
// Возвращаем true, если игра закончилась нормально (не выходом)
return g.State != quit
}
// saveCheck проверяет, является ли ввод командой сохранения
func (g *Game) saveCheck(input string) bool {
// Проверяем, если пользователь ввёл только "save" без имени файла
if input == "save" {
fmt.Println("Error: missing filename. " +
"Please use the format: save filename")
return true
}
// Проверяем команду сохранения с именем файла
if len(input) > 5 && input[:5] == "save " {
filename := input[5:]
// Проверяем, что имя файла не пустое
if len(strings.TrimSpace(filename)) == 0 {
fmt.Println("Error: empty file name. " +
"Please use the format: save filename")
return true
}
fmt.Printf("Game saved to file: %s\n", filename)
shapshot := g.gameSnapshot()
if err := g.Saver.SaveGame(filename, shapshot); err != nil {
fmt.Printf("Error saving game: %v\n", err)
}
return true
}
return false
}

View File

@@ -0,0 +1,82 @@
package game
import (
"bufio"
"fmt"
b "tic-tac-toe/board"
m "tic-tac-toe/model"
p "tic-tac-toe/player"
s "tic-tac-toe/storage"
)
// PrepareForSave подготавливает игру к сохранению
func (g *Game) PrepareForSave() {
// Устанавливаем флаг текущего игрока
g.IsCurrentFirst = (g.CurrentPlayer == g.Player)
}
func (g *Game) gameSnapshot() *m.GameSnapshot {
g.PrepareForSave()
return &m.GameSnapshot{
Board: g.Board,
PlayerFigure: g.Player.GetFigure(),
State: int(g.State),
Mode: int(g.Mode),
Difficulty: g.Difficulty,
IsCurrentFirst: g.IsCurrentFirst,
}
}
func (g *Game) RestoreFromSnapshot(
snapshot *m.GameSnapshot,
reader *bufio.Reader,
saver s.IGameSaver,
) {
g.Board = snapshot.Board
g.State = GameState(snapshot.State)
g.Mode = GameMode(snapshot.Mode)
g.Difficulty = snapshot.Difficulty
g.IsCurrentFirst = snapshot.IsCurrentFirst
// Создаем объекты игроков
g.Player = &p.HumanPlayer{Figure: snapshot.PlayerFigure}
g.Reader = reader
g.Saver = saver
g.recreatePlayersAfterLoad(reader)
}
// RecreatePlayersAfterLoad восстанавливает объекты игроков после загрузки из JSON
func (g *Game) recreatePlayersAfterLoad(reader *bufio.Reader) {
// Создаем игроков в зависимости от режима игры
if g.Player == nil {
fmt.Println("Error: Player is nil")
return
}
playerFigure := g.Player.GetFigure()
g.Player = p.NewHumanPlayer(playerFigure, reader)
// Получаем фигуру второго игрока
var player2Figure b.BoardField
if playerFigure == b.Cross {
player2Figure = b.Nought
} else {
player2Figure = b.Cross
}
// Создаем второго игрока в зависимости от режима
if g.Mode == PlayerVsPlayer {
g.Player2 = p.NewHumanPlayer(player2Figure, reader)
} else {
g.Player2 = p.NewComputerPlayer(player2Figure, g.Difficulty)
}
// Восстанавливаем указатель на текущего игрока
if g.IsCurrentFirst {
g.CurrentPlayer = g.Player
} else {
g.CurrentPlayer = g.Player2
}
}

View File

@@ -0,0 +1,116 @@
package game
import (
"bufio"
"fmt"
"strconv"
"strings"
b "tic-tac-toe/board"
p "tic-tac-toe/player"
s "tic-tac-toe/storage"
)
// SetupGame создает новую игру с пользовательскими настройками
func SetupGame(reader *bufio.Reader, saver s.IGameSaver) *Game {
// Запрашиваем размер игрового поля
size := getBoardSize(reader)
// Создаем доску
board := *b.NewBoard(size)
// Запрашиваем режим игры
mode := getGameMode(reader)
// Если выбран режим против компьютера, запрашиваем сложность
var difficulty p.Difficulty
if mode == PlayerVsComputer {
difficulty = getDifficulty(reader)
}
// Создаем новую игру
return NewGame(board, reader, saver, mode, difficulty)
}
// getBoardSize запрашивает у пользователя размер доски
func getBoardSize(reader *bufio.Reader) int {
size := b.BoardDefaultSize
var err error
for {
fmt.Printf("Choose board size (min: %d, max: %d, default: %d): ",
b.BoardMinSize, b.BoardMaxSize, b.BoardDefaultSize)
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
// Если пользователь не ввел ничего, используем размер по умолчанию
if input == "" {
return b.BoardDefaultSize
}
// Пытаемся преобразовать ввод в число
size, err = strconv.Atoi(input)
if err != nil || size < b.BoardMinSize || size > b.BoardMaxSize {
fmt.Println("Invalid input. Please try again!")
continue
}
return size
}
}
// getGameMode запрашивает у пользователя режим игры
func getGameMode(reader *bufio.Reader) GameMode {
for {
fmt.Println("Choose game mode:")
fmt.Println("1 - Player vs Player (PvP)")
fmt.Println("2 - Player vs Computer (PvC)")
fmt.Print("Your choice: ")
input, err := reader.ReadString('\n')
input = strings.TrimSpace(input)
if err != nil {
fmt.Println("Invalid input. Please try again!")
continue
}
switch input {
case "1":
return PlayerVsPlayer
case "2":
return PlayerVsComputer
default:
fmt.Println("Invalid input. Please try again!")
}
}
}
// getDifficulty запрашивает у пользователя уровень сложности компьютера
func getDifficulty(reader *bufio.Reader) p.Difficulty {
for {
fmt.Println("Choose computer difficulty:")
fmt.Println("1 - Easy (random moves)")
fmt.Println("2 - Medium (block winning moves)")
fmt.Println("3 - Hard (optimal strategy)")
fmt.Print("Your choice: ")
input, err := reader.ReadString('\n')
input = strings.TrimSpace(input)
if err != nil {
fmt.Println("Invalid input. Please try again!")
continue
}
switch input {
case "1":
return p.Easy
case "2":
return p.Medium
case "3":
return p.Hard
default:
fmt.Println("Invalid input. Please try again!")
}
}
}

View File

@@ -0,0 +1,20 @@
package game
type GameState int
// состояние игрового процесса
const (
playing GameState = iota
draw
crossWin
noughtWin
quit
)
// Режим игры
type GameMode int
const (
PlayerVsPlayer GameMode = iota
PlayerVsComputer
)

View File

@@ -0,0 +1,3 @@
module tic-tac-toe
go 1.24.0

View File

@@ -0,0 +1,69 @@
package main
import (
"bufio"
"fmt"
"os"
"strings"
"tic-tac-toe/game"
"tic-tac-toe/storage"
)
func main() {
reader := bufio.NewReader(os.Stdin)
gameStorage := storage.NewJsonGameStorage()
for {
fmt.Println("Welcome to Tic-Tac-Toe!")
fmt.Println("1 - Load game")
fmt.Println("2 - New game")
fmt.Println("q - Exit")
fmt.Print("Your choice: ")
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
switch input {
case "1":
// Загрузка сохраненной игры
loadedGame := &game.Game{}
for {
fmt.Println("Input file name: ")
fileName, _ := reader.ReadString('\n')
fileName = strings.TrimSpace(fileName)
snapshote, err := gameStorage.LoadGame(fileName)
if err != nil {
fmt.Println("Error loading game: ", err)
continue
}
// Восстанавливаем все необходимые поля игры
loadedGame.RestoreFromSnapshot(
snapshote, reader,
gameStorage.(storage.IGameSaver),
)
break
}
// Запускаем игру
loadedGame.Play()
case "2":
// Создаем новую игру с помощью диалога настройки
newGame := game.SetupGame(reader, gameStorage.(storage.IGameSaver))
// Запускаем игру
newGame.Play()
case "q":
fmt.Println("Goodbye!")
return
default:
fmt.Println("Invalid choice. Please try again.")
}
}
}

View File

@@ -0,0 +1,16 @@
package model
import (
b "tic-tac-toe/board"
p "tic-tac-toe/player"
)
// Структура для сериализации/десериализации игры
type GameSnapshot struct {
Board *b.Board `json:"board"`
PlayerFigure b.BoardField `json:"player_figure"`
State int `json:"state"`
Mode int `json:"mode"`
Difficulty p.Difficulty `json:"difficulty,omitempty"`
IsCurrentFirst bool `json:"is_current_first"`
}

View File

@@ -0,0 +1,310 @@
package game
import (
"fmt"
"math/rand"
b "tic-tac-toe/board"
"time"
)
// Уровни сложности компьютера
type Difficulty int
const (
Easy Difficulty = iota
Medium
Hard
)
// ComputerPlayer представляет игрока-компьютера
type ComputerPlayer struct {
Figure b.BoardField `json:"figure"`
Difficulty Difficulty `json:"difficulty"`
rand *rand.Rand
}
// NewComputerPlayer создает нового игрока-компьютера с заданным уровнем сложности
func NewComputerPlayer(
figure b.BoardField,
difficulty Difficulty,
) *ComputerPlayer {
source := rand.NewSource(time.Now().UnixNano())
return &ComputerPlayer{
Figure: figure,
Difficulty: difficulty,
rand: rand.New(source),
}
}
// GetSymbol возвращает символ игрока
func (p *ComputerPlayer) GetSymbol() string {
if p.Figure == b.Cross {
return "X"
}
return "O"
}
// SwitchPlayer изменяет фигуру текущего игрока
func (p *ComputerPlayer) SwitchPlayer() {
if p.Figure == b.Cross {
p.Figure = b.Nought
} else {
p.Figure = b.Cross
}
}
// GetFigure возвращает текущую фигуру игрока
func (p *ComputerPlayer) GetFigure() b.BoardField {
return p.Figure
}
// IsComputer возвращает true для компьютера
func (p *ComputerPlayer) IsComputer() bool {
return true
}
// MakeMove реализует ход компьютера в зависимости от выбранной сложности
func (p *ComputerPlayer) MakeMove(board *b.Board) (int, int, bool) {
fmt.Printf("%s (Computer) making move... ", p.GetSymbol())
var row, col int
switch p.Difficulty {
case Easy:
row, col = p.makeEasyMove(board)
case Medium:
row, col = p.makeMediumMove(board)
case Hard:
row, col = p.makeHardMove(board)
}
fmt.Printf("Move made (%d, %d)\n", row+1, col+1)
return row, col, true
}
// Легкий уровень: случайный ход на свободную клетку
func (p *ComputerPlayer) makeEasyMove(board *b.Board) (int, int) {
emptyCells := p.getEmptyCells(board)
if len(emptyCells) == 0 {
return -1, -1
}
// Выбираем случайную свободную клетку
randomIndex := p.rand.Intn(len(emptyCells))
return emptyCells[randomIndex][0], emptyCells[randomIndex][1]
}
// Средний уровень: проверяет возможность выигрыша
// или блокировки выигрыша противника
func (p *ComputerPlayer) makeMediumMove(board *b.Board) (int, int) {
// Проверяем, можем ли мы выиграть за один ход
if move := p.findWinningMove(board, p.Figure); move != nil {
return move[0], move[1]
}
// Проверяем, нужно ли блокировать победу противника
opponentFigure := b.Cross
if p.Figure == b.Cross {
opponentFigure = b.Nought
}
if move := p.findWinningMove(board, opponentFigure); move != nil {
return move[0], move[1]
}
// Занимаем центр, если свободен (хорошая стратегия)
center := board.Size / 2
if board.Board[center][center] == b.Empty {
return center, center
}
// Занимаем угол, если свободен
corners := [][]int{
{0, 0},
{0, board.Size - 1},
{board.Size - 1, 0},
{board.Size - 1, board.Size - 1},
}
for _, corner := range corners {
if board.Board[corner[0]][corner[1]] == b.Empty {
return corner[0], corner[1]
}
}
// Если нет лучшего хода, делаем случайный ход
return p.makeEasyMove(board)
}
// Сложный уровень: использует алгоритм минимакс для оптимального хода
func (p *ComputerPlayer) makeHardMove(board *b.Board) (int, int) {
// Если доска пустая, ходим в центр или угол (оптимальный первый ход)
emptyCells := p.getEmptyCells(board)
if len(emptyCells) == board.Size*board.Size {
// Первый ход - центр или угол
center := board.Size / 2
// 50% шанс выбрать центр на нечетной доске
if p.rand.Intn(2) == 0 && board.Size%2 == 1 {
return center, center
} else {
corners := [][]int{
{0, 0},
{0, board.Size - 1},
{board.Size - 1, 0},
{board.Size - 1, board.Size - 1},
}
randomCorner := corners[p.rand.Intn(len(corners))]
return randomCorner[0], randomCorner[1]
}
}
// Для небольших досок (3x3) используем полный минимакс
if board.Size <= 3 {
bestScore := -1000
bestMove := []int{-1, -1}
// Рассматриваем все свободные клетки
for _, cell := range emptyCells {
row, col := cell[0], cell[1]
// Пробуем сделать ход
board.Board[row][col] = p.Figure
// Вычисляем оценку хода через минимакс
score := p.minimax(board, 0, false)
// Возвращаем клетку в исходное состояние
board.Board[row][col] = b.Empty
// Обновляем лучший ход
if score > bestScore {
bestScore = score
bestMove = []int{row, col}
}
}
return bestMove[0], bestMove[1]
}
// Для больших досок используем стратегию среднего уровня,
// так как полный минимакс будет слишком ресурсоемким
return p.makeMediumMove(board)
}
// Алгоритм минимакс для определения оптимального хода
func (p *ComputerPlayer) minimax(
board *b.Board,
depth int, isMaximizing bool,
) int {
opponentFigure := b.Cross
if p.Figure == b.Cross {
opponentFigure = b.Nought
}
// Проверяем терминальное состояние
if board.CheckWin(p.Figure) {
return 10 - depth // Выигрыш, чем быстрее, тем лучше
} else if board.CheckWin(opponentFigure) {
return depth - 10 // Проигрыш, чем дольше, тем лучше
} else if board.CheckDraw() {
return 0 // Ничья
}
emptyCells := p.getEmptyCells(board)
if isMaximizing {
bestScore := -1000
// Проходим по всем свободным клеткам
for _, cell := range emptyCells {
row, col := cell[0], cell[1]
// Делаем ход
board.Board[row][col] = p.Figure
// Рекурсивно оцениваем ход
score := p.minimax(board, depth+1, false)
// Отменяем ход
board.Board[row][col] = b.Empty
bestScore = max(score, bestScore)
}
return bestScore
} else {
bestScore := 1000
// Проходим по всем свободным клеткам
for _, cell := range emptyCells {
row, col := cell[0], cell[1]
// Делаем ход противника
board.Board[row][col] = opponentFigure
// Рекурсивно оцениваем ход
score := p.minimax(board, depth+1, true)
// Отменяем ход
board.Board[row][col] = b.Empty
bestScore = min(score, bestScore)
}
return bestScore
}
}
// Вспомогательная функция для поиска хода, приводящего к выигрышу
func (p *ComputerPlayer) findWinningMove(
board *b.Board,
figure b.BoardField,
) []int {
for _, cell := range p.getEmptyCells(board) {
row, col := cell[0], cell[1]
// Пробуем сделать ход
board.Board[row][col] = figure
// Проверяем, приведет ли этот ход к выигрышу
if board.CheckWin(figure) {
// Отменяем ход и возвращаем координаты
board.Board[row][col] = b.Empty
return []int{row, col}
}
// Отменяем ход
board.Board[row][col] = b.Empty
}
return nil // Нет выигрышного хода
}
// Получение списка пустых клеток
func (p *ComputerPlayer) getEmptyCells(board *b.Board) [][]int {
var emptyCells [][]int
for i := 0; i < board.Size; i++ {
for j := 0; j < board.Size; j++ {
if board.Board[i][j] == b.Empty {
emptyCells = append(emptyCells, []int{i, j})
}
}
}
return emptyCells
}
// Вспомогательные функции max и min
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

View File

@@ -0,0 +1,22 @@
package game
import b "tic-tac-toe/board"
// IPlayer представляет интерфейс для любого игрока (человека или компьютера)
type IPlayer interface {
// Получение символа игрока (X или O)
GetSymbol() string
// Переключение хода на другого игрока
SwitchPlayer()
// Получение текущей фигуры игрока
GetFigure() b.BoardField
// Выполнение хода игрока
// Возвращает координаты хода (x, y) и признак успешности
MakeMove(board *b.Board) (int, int, bool)
// Проверка, является ли игрок компьютером
IsComputer() bool
}

View File

@@ -0,0 +1,104 @@
package game
import (
"bufio"
"fmt"
"strconv"
"strings"
b "tic-tac-toe/board"
)
// HumanPlayer представляет игрока-человека
type HumanPlayer struct {
Figure b.BoardField `json:"figure"`
Reader *bufio.Reader `json:"-"`
}
// NewHumanPlayer создает нового игрока-человека
func NewHumanPlayer(figure b.BoardField, reader *bufio.Reader) *HumanPlayer {
return &HumanPlayer{Figure: figure, Reader: reader}
}
// GetSymbol возвращает символ игрока
func (p *HumanPlayer) GetSymbol() string {
if p.Figure == b.Cross {
return "X"
}
return "O"
}
// SwitchPlayer изменяет фигуру текущего игрока
func (p *HumanPlayer) SwitchPlayer() {
if p.Figure == b.Cross {
p.Figure = b.Nought
} else {
p.Figure = b.Cross
}
}
// GetFigure возвращает текущую фигуру игрока
func (p *HumanPlayer) GetFigure() b.BoardField {
return p.Figure
}
// MakeMove обрабатывает строку ввода от человека и преобразует её в координаты хода
// input - строка ввода в формате "1 2"
func (p *HumanPlayer) MakeMove(board *b.Board) (int, int, bool) {
fmt.Printf(
"%s's turn. Enter row and column (e.g. 1 2): ",
p.GetSymbol(),
)
input, err := p.Reader.ReadString('\n')
input = strings.TrimSpace(input)
if err != nil {
fmt.Println("Invalid input. Please try again.")
return -1, -1, false
}
return p.ParseMove(input, board)
}
// ParseMove обрабатывает строку ввода от человека и преобразует её в координаты хода
func (p *HumanPlayer) ParseMove(
input string,
board *b.Board,
) (int, int, bool) {
parts := strings.Fields(input)
if len(parts) != 2 {
fmt.Println("Invalid input. Please try again.")
return -1, -1, false
}
row, err1 := strconv.Atoi(parts[0])
col, err2 := strconv.Atoi(parts[1])
if err1 != nil || err2 != nil ||
row < 1 || col < 1 || row > board.Size ||
col > board.Size {
fmt.Println("Invalid input. Please try again.")
return -1, -1, false
}
// Преобразуем введенные координаты (начиная с 1) в индексы массива (начиная с 0)
return row - 1, col - 1, true
}
// IsComputer возвращает false для человека-игрока
func (p *HumanPlayer) IsComputer() bool {
return false
}
// Для обратной совместимости
type Player HumanPlayer
func NewPlayer() *Player {
return (*Player)(NewHumanPlayer(b.Cross, nil))
}
func (p *Player) SwitchPlayer() {
(*HumanPlayer)(p).SwitchPlayer()
}
func (p *Player) GetSymbol() string {
return (*HumanPlayer)(p).GetSymbol()
}

View File

@@ -0,0 +1 @@
{"board":{"board":[[1,2,0],[0,1,0],[0,2,0]],"size":3},"player_figure":1,"state":0,"mode":1,"is_current_first":true}

View File

@@ -0,0 +1,16 @@
package storage
import m "tic-tac-toe/model"
type IGameLoader interface {
LoadGame(path string) (*m.GameSnapshot, error)
}
type IGameSaver interface {
SaveGame(path string, game *m.GameSnapshot) error
}
type IGameStorage interface {
IGameLoader
IGameSaver
}

View File

@@ -0,0 +1,52 @@
package storage
import (
"encoding/json"
"os"
"strings"
m "tic-tac-toe/model"
)
func NewJsonGameStorage() IGameStorage {
return &JsonGameStorage{}
}
type JsonGameStorage struct{}
func (j *JsonGameStorage) LoadGame(path string) (*m.GameSnapshot, error) {
if !strings.HasSuffix(path, ".json") {
path += ".json"
}
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
decoder := json.NewDecoder(file)
var snapshot m.GameSnapshot
err = decoder.Decode(&snapshot)
if err != nil {
return nil, err
}
return &snapshot, nil
}
func (j *JsonGameStorage) SaveGame(path string, game *m.GameSnapshot) error {
if !strings.HasSuffix(path, ".json") {
path += ".json"
}
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
encoder := json.NewEncoder(file)
err = encoder.Encode(game)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Go App",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}",
"console": "integratedTerminal"
}
]
}

View File

@@ -0,0 +1,94 @@
package db
import (
"database/sql"
"fmt"
"log"
"os"
)
type SQLiteRepository struct {
db *sql.DB
}
func createDB(pathToDB string) *sql.DB {
db, err := sql.Open("sqlite3", pathToDB)
if err != nil {
log.Fatal(err)
}
db.Exec(ProjectTabelDefinition)
db.Exec(TaskTabelDefinition)
return db
}
func NewSQLiteRepository() *SQLiteRepository {
var db *sql.DB
if _, err := os.Stat(dbName); os.IsNotExist(err) {
db = createDB(dbName)
fmt.Println("DB isn't exist")
putDefaultValuesToDB(&SQLiteRepository{
db: db,
})
} else {
db, err = sql.Open("sqlite3", dbName)
if err != nil {
log.Fatal(err)
}
fmt.Println("DB already exists")
}
return &SQLiteRepository{
db: db,
}
}
func putDefaultValuesToDB(rep *SQLiteRepository) {
firstProject, _ := rep.AddProject(Project{
Name: "Go",
Description: "Roadmap for learning Go",
})
secondProject, _ := rep.AddProject(Project{
Name: "One Year",
Description: "Tasks for the year",
})
rep.AddTask(Task{
Name: "Variable",
Description: "Learning Go build-in variables",
Priority: 1,
}, firstProject.ID)
rep.AddTask(Task{
Name: "Struct",
Description: "Learning use struct in OOP code",
Priority: 3,
}, firstProject.ID)
rep.AddTask(Task{
Name: "Goroutine",
Description: "Learning concurrent programming",
Priority: 5,
}, firstProject.ID)
rep.AddTask(Task{
Name: "DataBase",
Description: "How write app with db",
Priority: 1,
}, firstProject.ID)
rep.AddTask(Task{
Name: "PhD",
Description: "Ph.D. in Technical Sciences",
Priority: 5,
}, secondProject.ID)
rep.AddTask(Task{
Name: "Losing weight",
Description: "Exercise and eat less chocolate",
Priority: 2,
}, secondProject.ID)
rep.AddTask(Task{
Name: "Пафос и превозмогание",
Description: "10к подписчиков на канале",
Priority: 2,
}, secondProject.ID)
}
func (s *SQLiteRepository) Close() {
s.db.Close()
}

View File

@@ -0,0 +1,31 @@
package db
import "errors"
const dbName = "todo.db"
var (
ErrDuplicate = errors.New("record already exists")
ErrNotExists = errors.New("row not exists")
ErrUpdateFailed = errors.New("update failed")
ErrDeleteFailed = errors.New("delete failed")
)
const ProjectTabelDefinition = `
CREATE TABLE IF NOT EXISTS projects(
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT UNIQUE,
description TEXT
);
`
const TaskTabelDefinition = `
CREATE TABLE IF NOT EXISTS tasks(
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL,
description TEXT NOT NULL,
priority INTEGER NOT NULL,
is_done BOOLEAN NOT NULL CHECK (is_done IN (0, 1)),
project_id INTEGER not null references projects(id)
);
`

View File

@@ -0,0 +1,20 @@
package db
type Project struct {
ID int
Name string
Description string
}
type Task struct {
ID int
Name string
Description string
Priority uint8
IsDone bool
}
type ProjectTask struct {
Task
ProjectID int
}

View File

@@ -0,0 +1,92 @@
package db
import (
"errors"
"github.com/mattn/go-sqlite3"
)
// Метод для добавления проекта в базу данных
func (s *SQLiteRepository) AddProject(project Project) (*Project, error) {
res, err := s.db.Exec( // Запрос на добавление проекта
// Используем экранирование данных для предотвращения SQL-инъекции
// данные из переменных project.Name и project.Description
// будут вставлены в запрос в место знаков вопроса
// в пордке их перечисления
"INSERT INTO projects(name, description) values(?,?)",
project.Name, project.Description,
)
if err != nil { // Если произошла ошибка
var sqliteErr sqlite3.Error
// Если такой проект уже существует
if errors.As(err, &sqliteErr) {
if errors.Is(
sqliteErr.ExtendedCode,
sqlite3.ErrConstraintUnique) {
return nil, ErrDuplicate // Возвращаем ErrDuplicate
}
}
return nil, err
}
id, err := res.LastInsertId() // Получаем ID
if err != nil {
return nil, err
}
project.ID = int(id) // Устанавливаем ID проекту
return &project, nil
}
// Метод для удаления проекта из базы данных
func (s *SQLiteRepository) DeleteProject(projectID int) error {
// Запрос на удаление проекта из таблицы projects
s.db.Exec("DELETE FROM projects WHERE id = ?", projectID)
// Запрос на удаление всех задач, связанных с проектом
res, err := s.db.Exec(
"DELETE FROM tasks WHERE project_id = ?",
projectID,
)
if err != nil {
return err
}
// Проверяем, были ли удалены задачи
rowsAffected, err := res.RowsAffected()
if err != nil {
return err
}
// Если не было удалено ни одной задачи
if rowsAffected == 0 {
return ErrDeleteFailed
}
return err
}
// Метод для получения всех проектов
func (s *SQLiteRepository) GetAllProjects() ([]Project, error) {
// Запрос на получение всех проектов
rows, err := s.db.Query("SELECT * FROM projects")
if err != nil {
return nil, err
}
defer rows.Close() // Закрываем соединение
// Перебираем результаты запроса и добавляем в []Project
var projects []Project
for rows.Next() {
var project Project
// считываем данные из каждой строки, в соответствующие
// поля структуры Project
if err := rows.Scan(&project.ID, &project.Name,
&project.Description); err != nil {
return nil, err
}
// Добавляем проект в срез
projects = append(projects, project)
}
return projects, nil
}

View File

@@ -0,0 +1,129 @@
package db
import "errors"
// Метод для добавления задачи в базу данных
func (s *SQLiteRepository) AddTask(task Task, projectID int) (*Task, error) {
// Запрос на добавление задачи проекту с id == projectID
res, err := s.db.Exec(
"INSERT INTO tasks(name, description, priority,"+
" is_done, project_id) values(?,?,?,?,?)",
task.Name, task.Description, task.Priority,
task.IsDone, projectID,
)
if err != nil {
return nil, err
}
id, err := res.LastInsertId() // Получаем ID
if err != nil {
return nil, err
}
task.ID = int(id) // Устанавливаем ID задаче
return &task, nil
}
// Метод для удаления задачи из базы данных
func (s *SQLiteRepository) DeleteTask(taskID int) error {
// Запрос на удаление задачи из таблицы tasks
res, err := s.db.Exec(
"DELETE FROM tasks WHERE id = ?",
taskID,
)
if err != nil {
return err
}
// Проверяем, была ли удалена задача
rowsAffected, err := res.RowsAffected()
if err != nil {
return err
}
// Если не была удалена ни одна задача
if rowsAffected == 0 {
return ErrDeleteFailed
}
return err
}
// Метод для получения всех задач
func (s *SQLiteRepository) GetAllTasks() (tasks []ProjectTask, err error) {
// Запрос на получение всех задач
rows, err := s.db.Query("SELECT * FROM tasks")
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var task ProjectTask
// считываем данные из каждой строки, в соответствующие
// поля структуры ProjectTask
if err := rows.Scan(&task.ID, &task.Name,
&task.Description, &task.Priority,
&task.IsDone, &task.ProjectID); err != nil {
return nil, err
}
// Добавляем задачу в срез
tasks = append(tasks, task)
}
return
}
// Метод для получения всех задач конкретного проекта
func (s *SQLiteRepository) GetProjectTasks(projectID int) (tasks []Task, err error) {
// Запрос на получение всех задач у проекта с заданным id
rows, err := s.db.Query(
"SELECT * FROM tasks WHERE project_id = ?",
projectID,
)
if err != nil {
return nil, err
}
defer rows.Close() // Закрываем соединение
for rows.Next() {
var task Task
var progID int
// считываем данные из каждой строки, в соответствующие
// поля структуры Task
if err := rows.Scan(&task.ID, &task.Name, &task.Description,
&task.Priority, &task.IsDone, &progID); err != nil {
return nil, err
}
tasks = append(tasks, task)
}
return
}
// Метод для обновления задачи
func (s *SQLiteRepository) TaskDone(taskId int) error {
if taskId == 0 { // Проверка на валидность
return errors.New("invalid updated ID")
}
// Запрос на перевод задачи с указанным id
// в состояние "выполнена"
res, err := s.db.Exec(
"UPDATE tasks SET is_done = ? WHERE id = ?", 1,
taskId,
)
if err != nil {
return err
}
// Проверяем, была ли обновлена задача
rowsAffected, err := res.RowsAffected()
if err != nil {
return err
}
// Если не была обновлена ни одна задача
if rowsAffected == 0 {
return ErrUpdateFailed
}
return nil
}

View File

@@ -0,0 +1,12 @@
module golang/todo
go 1.24
require (
github.com/daviddengcn/go-colortext v1.0.0 // indirect
github.com/dixonwille/wmenu v4.0.2+incompatible // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.24 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
gopkg.in/dixonwille/wlog.v2 v2.0.0 // indirect
)

View File

@@ -0,0 +1,17 @@
github.com/daviddengcn/go-colortext v1.0.0 h1:ANqDyC0ys6qCSvuEK7l3g5RaehL/Xck9EX8ATG8oKsE=
github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c=
github.com/dixonwille/wmenu v4.0.2+incompatible h1:lxrPJsx9LpdUFD5T+dOfl6gPKLbBmiAtEdACLT1I2/w=
github.com/dixonwille/wmenu v4.0.2+incompatible/go.mod h1:DnajdZEKFQksxBctWekpWaQXQrDUHRBco6b8MyZnR1s=
github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho=
github.com/golangplus/bytes v1.0.0/go.mod h1:AdRaCFwmc/00ZzELMWb01soso6W1R/++O1XL80yAn+A=
github.com/golangplus/fmt v1.0.0/go.mod h1:zpM0OfbMCjPtd2qkTD/jX2MgiFCqklhSUFyDW44gVQE=
github.com/golangplus/testing v1.0.0/go.mod h1:ZDreixUV3YzhoVraIDyOzHrr76p6NUh6k/pPg/Q3gYA=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/dixonwille/wlog.v2 v2.0.0 h1:TbGWtD8ahWVSihKKr+z2Dw7Cv/7IrfN6dwrcrre17pU=
gopkg.in/dixonwille/wlog.v2 v2.0.0/go.mod h1:JYQHRnhGPLno/iATOiGkEXoRanJXqdz9Qo6/QwfARUc=

View File

@@ -0,0 +1,19 @@
package main
import (
"golang/todo/db"
"golang/todo/menu"
"time"
)
func main() {
// Создаем репозиторий
rep := db.NewSQLiteRepository()
// Создаем отложенное закрытие соединения
defer rep.Close()
// Бесконечный цикл
for {
menu.CreateMenu(rep)
time.Sleep(2 * time.Second)
}
}

View File

@@ -0,0 +1,101 @@
package menu
import (
"bufio"
"fmt"
"golang/todo/db"
"log"
"os"
"strconv"
"strings"
"github.com/dixonwille/wmenu"
)
// Функция для создания меню
func CreateMenu(rep *db.SQLiteRepository) {
menu := wmenu.NewMenu("What would you like to do?")
menu.Action(func(opts []wmenu.Opt) error {
handleFunc(rep, opts)
return nil
})
menu.Option("Add a new Project", 0, false, nil)
menu.Option("Delete a Project by ID", 1, false, nil)
menu.Option("Get all Projects", 2, false, nil)
menu.Option("Add a Task", 3, false, nil)
// Выбор по умолчанию. Если пользователь жмякнет Enter,
// не выбирая никакого пункта меню,
// то будет выполнен этот пункт
menu.Option("Get all Tasks", 4, true, nil)
menu.Option("Get all Project tasks", 5, false, nil)
menu.Option("Done a Task by ID", 6, false, nil)
menu.Option("Delete a Task by ID", 7, false, nil)
menu.Option("Quit Application", 8, false, nil)
menuerr := menu.Run()
fmt.Println()
fmt.Println("---------------------------------")
if menuerr != nil {
log.Fatal(menuerr)
}
}
// Функция для обработки ввода выбранного пункта меню
func handleFunc(rep *db.SQLiteRepository, opts []wmenu.Opt) {
switch opts[0].Value {
case 0:
fmt.Println("Adding a new Project")
addProject(rep)
case 1:
fmt.Println("Deleting a Project by ID")
deleteProjectByID(rep)
case 2:
fmt.Println("Getting all Projects")
getAllProjects(rep)
case 3:
fmt.Println("Adding a new Task")
addTask(rep)
case 4:
fmt.Println("Getting all Tasks")
getAllTasks(rep)
case 5:
fmt.Println("Getting all Project tasks by ProjectID")
getAllProjectTasks(rep)
case 6:
fmt.Println("Doing a Task by ID")
doneTask(rep)
case 7:
fmt.Println("Deleting a Task by ID")
deleteTaskByID(rep)
case 8:
fmt.Println("See you later!!!")
os.Exit(0)
}
}
// Функция для вывода сообщения о невалидных данных
func printNotValidData() {
fmt.Println("Data is not valid!!!")
}
// Функция для получения числового значения из потока ввода
func getIntValueFromStd(reader *bufio.Reader) (int, error) {
tempID, _, _ := reader.ReadLine()
idStr := strings.TrimSuffix(string(tempID), "\n")
idProj, err := strconv.Atoi(idStr)
if err != nil {
return 0, err
}
return idProj, nil
}
// Функция для получения строки из потока ввода
func getStringValueFromStd(reader *bufio.Reader) (string, error) {
data, err := reader.ReadString('\n')
data = strings.TrimSuffix(data, "\r\n")
if err != nil {
return "", err
}
return data, nil
}

View File

@@ -0,0 +1,71 @@
package menu
import (
"bufio"
"fmt"
"golang/todo/db"
"os"
)
// Функция для добавления нового проекта
func addProject(rep *db.SQLiteRepository) {
project := db.Project{} // Создаем новый проект
reader := bufio.NewReader(os.Stdin) // Создаем поток ввода
fmt.Print("Input project name: ")
name, _ := getStringValueFromStd(reader)
fmt.Print("Input description project: ")
desc, _ := getStringValueFromStd(reader)
project.Name = name
project.Description = desc
// Если название и описание проекта не пустые
if project.Name != "" && project.Description != "" {
project, err := rep.AddProject(project) // Добавляем проект
if err != nil {
fmt.Println(err)
} else {
// Выводим информацию о добавленном проекте
fmt.Printf("\nAdded project: %+v\n", *project)
}
} else {
// Выводим сообщение об ошибке
printNotValidData()
}
}
// Функция для удаления проекта
func deleteProjectByID(rep *db.SQLiteRepository) {
fmt.Print("Input ID for deleting project: ")
id, err := getIntValueFromStd(bufio.NewReader(os.Stdin))
if err != nil {
printNotValidData()
return
}
err = rep.DeleteProject(id)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("Project deleted")
}
// Функция для получения всех проектов
func getAllProjects(rep *db.SQLiteRepository) {
progects, err := rep.GetAllProjects()
if err != nil {
printNotValidData()
return
}
if len(progects) == 0 {
fmt.Println("You don't have any project")
} else {
fmt.Println("You current projects:")
for _, it := range progects {
fmt.Printf("ProjectID: %v || Name: %v || Desc: %v\n",
it.ID, it.Name, it.Description)
}
}
}

View File

@@ -0,0 +1,136 @@
package menu
import (
"bufio"
"fmt"
"golang/todo/db"
"os"
)
// Функция для добавления новой задачи
func addTask(rep *db.SQLiteRepository) {
task := db.Task{}
reader := bufio.NewReader(os.Stdin)
fmt.Print("Input project ID: ")
projectID, err := getIntValueFromStd(reader)
if err != nil {
printNotValidData()
return
}
fmt.Print("Input task name: ")
name, _ := getStringValueFromStd(reader)
fmt.Print("Input description task: ")
desc, _ := getStringValueFromStd(reader)
fmt.Print("Input priority task: ")
priority, err := getIntValueFromStd(reader)
if err != nil {
printNotValidData()
return
}
task.Name = name
task.Description = desc
task.Priority = uint8(priority)
if task.Name != "" && task.Description != "" {
task, err := rep.AddTask(task, projectID)
if err != nil {
fmt.Println(err)
}
fmt.Printf("\nAdded task: %+v\n", *task)
} else {
printNotValidData()
}
}
// Функция для удаления задачи
func deleteTaskByID(rep *db.SQLiteRepository) {
fmt.Print("Input ID for deleting task: ")
id, err := getIntValueFromStd(bufio.NewReader(os.Stdin))
if err != nil {
printNotValidData()
return
}
err = rep.DeleteTask(id)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("Task deleted")
}
// Функция для получения всех задач
func getAllTasks(rep *db.SQLiteRepository) {
tasks, err := rep.GetAllTasks()
if err != nil {
printNotValidData()
return
}
if len(tasks) == 0 {
fmt.Println("You don't have any task")
} else {
fmt.Println("You current tasks: ")
for _, it := range tasks {
fmt.Printf("TaskID: %v || Name: %v || Desc: %v ||"+
" Priority: %v || IsDone: %v || ProjID: %v\n",
it.ID, it.Name, it.Description, it.Priority,
it.IsDone, it.ProjectID,
)
}
}
}
// Функция для получения всех задач проекта
func getAllProjectTasks(rep *db.SQLiteRepository) {
fmt.Print("Input ID for project: ")
id, err := getIntValueFromStd(bufio.NewReader(os.Stdin))
if err != nil {
printNotValidData()
return
}
tasks, err := rep.GetProjectTasks(id)
if err != nil {
fmt.Println(err)
return
}
if len(tasks) == 0 {
fmt.Println("You don't have any task")
} else {
fmt.Printf(
"Project with ID = %d have next tasks:\n",
id,
)
for _, it := range tasks {
fmt.Printf("TaskID: %v || Name: %v || Desc: %v ||"+
" Priority: %v || IsDone: %v\n",
it.ID, it.Name, it.Description,
it.Priority, it.IsDone,
)
}
}
}
// Функция для перевода задачи в состояние "Выполнено"
func doneTask(rep *db.SQLiteRepository) {
fmt.Print("Input task ID: ")
id, err := getIntValueFromStd(bufio.NewReader(os.Stdin))
if err != nil {
printNotValidData()
return
}
err = rep.TaskDone(id)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("Congratulations! Task done!")
}

Binary file not shown.

View File

@@ -0,0 +1,94 @@
package db
import (
_ "database/sql"
"fmt"
"log"
"os"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type SQLiteRepository struct {
db *gorm.DB // заменили на *gorm.DB
}
func NewSQLiteRepository() *SQLiteRepository {
var db *gorm.DB
rep := &SQLiteRepository{}
// Если база данных не существует, то создаем ее
if _, err := os.Stat(dbName); os.IsNotExist(err) {
// Отквываем соединение с базой данных
db, err = gorm.Open(sqlite.Open(dbName), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
fmt.Println("DB isn't exist")
// Создаем таблицы
db.AutoMigrate(&Project{}, &ProjectTask{})
// Заполняем БД значениями по умолчанию
rep.db = db
putDefaultValuesToDB(rep)
} else {
// Отквываем соединение с базой данных
db, err = gorm.Open(sqlite.Open(dbName), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
rep.db = db
fmt.Println("DB already exists")
}
return rep
}
func putDefaultValuesToDB(rep *SQLiteRepository) {
firstProject, _ := rep.AddProject(Project{
Name: "Go",
Description: "Roadmap for learning Go",
})
secondProject, _ := rep.AddProject(Project{
Name: "One Year",
Description: "Tasks for the year",
})
rep.AddTask(Task{
Name: "Variable",
Description: "Learning Go build-in variables",
Priority: 1,
}, firstProject.ID)
rep.AddTask(Task{
Name: "Struct",
Description: "Learning use struct in OOP code",
Priority: 3,
}, firstProject.ID)
rep.AddTask(Task{
Name: "Goroutine",
Description: "Learning concurrent programming",
Priority: 5,
}, firstProject.ID)
rep.AddTask(Task{
Name: "DataBase",
Description: "How write app with db",
Priority: 1,
}, firstProject.ID)
rep.AddTask(Task{
Name: "PhD",
Description: "Ph.D. in Technical Sciences",
Priority: 5,
}, secondProject.ID)
rep.AddTask(Task{
Name: "Losing weight",
Description: "Exercise and eat less chocolate",
Priority: 2,
}, secondProject.ID)
rep.AddTask(Task{
Name: "Пафос и превозмогание",
Description: "10к подписчиков на канале",
Priority: 2,
}, secondProject.ID)
}
func (r *SQLiteRepository) Close() {
}

View File

@@ -0,0 +1,12 @@
package db
import "errors"
const dbName = "todo.db"
var (
ErrDuplicate = errors.New("record already exists")
ErrNotExists = errors.New("row not exists")
ErrUpdateFailed = errors.New("update failed")
ErrDeleteFailed = errors.New("delete failed")
)

View File

@@ -0,0 +1,21 @@
package db
type Project struct {
ID int `gorm:"primary_key;autoIncrement:true;not null"`
Name string `gorm:"unique;not null"`
Description string
}
type Task struct {
ID int `gorm:"primary_key;autoIncrement;not null"`
Name string `gorm:"not null"`
Description string `gorm:"not null"`
Priority uint8 `gorm:"not null"`
IsDone bool `gorm:"not null"`
}
type ProjectTask struct {
Task
ProjectID int `gorm:"not null"`
Project *Project `gorm:"foreignKey:ProjectID;references:ID"`
}

View File

@@ -0,0 +1,53 @@
package db
import (
"errors"
"github.com/mattn/go-sqlite3"
)
// Метод для добавления проекта в базу данных
func (r *SQLiteRepository) AddProject(project Project) (*Project, error) {
tx := r.db.Create(&project)
if tx.Error != nil {
var sqliteErr sqlite3.Error
if errors.As(tx.Error, &sqliteErr) {
if errors.Is(sqliteErr.ExtendedCode,
sqlite3.ErrConstraintUnique) {
return nil, ErrDuplicate
}
}
return nil, tx.Error
}
return &project, nil
}
// Метод для удаления проекта из базы данных
func (r *SQLiteRepository) DeleteProject(projectID int) error {
tx := r.db.Delete(&Project{ID: projectID})
if tx.Error != nil {
return tx.Error
}
rowsAffected := tx.RowsAffected
if rowsAffected == 0 {
return ErrDeleteFailed
}
return nil
}
// Метод для получения всех проектов
func (r *SQLiteRepository) GetAllProjects() ([]Project, error) {
var projects []Project
tx := r.db.Find(&projects)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, ErrNotExists
}
return projects, nil
}

View File

@@ -0,0 +1,95 @@
package db
import "errors"
// Метод для добавления задачи в базу данных
func (r *SQLiteRepository) AddTask(task Task, projectID int) (*Task, error) {
pjTask := &ProjectTask{ // создаем связь между задачей и проектом
Task: task,
ProjectID: projectID,
}
tx := r.db.Create(pjTask) // создаем задачу
if tx.Error != nil {
return nil, tx.Error
}
return &pjTask.Task, nil
}
// Метод для удаления задачи из базы данных
func (r *SQLiteRepository) DeleteTask(taskID int) error {
// удаляем задачу по ее ID
tx := r.db.Delete(&ProjectTask{Task: Task{ID: taskID}})
if tx.Error != nil {
return tx.Error
}
rowsAffected := tx.RowsAffected
if rowsAffected == 0 {
return ErrDeleteFailed
}
return nil
}
// Метод для получения всех задач
func (r *SQLiteRepository) GetAllTasks() (tasks []ProjectTask, err error) {
tx := r.db.Find(&tasks) // получаем все задачи
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, ErrNotExists
}
return
}
// Метод для получения всех задач конкретного проекта
func (r *SQLiteRepository) GetProjectTasks(projectID int) (tasks []Task, err error) {
if projectID == 0 {
return nil, errors.New("invalid updated ID")
}
var pjTasks []ProjectTask
// получаем все задачи конкретного проекта
// и добавляем их в срез pjTasks []ProjectTask
tx := r.db.Where("project_id", projectID).Find(&pjTasks)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, ErrNotExists
}
for _, it := range pjTasks {
// Отделяем задачу от проекта
// и добавляем в срез tasks
tasks = append(tasks, it.Task)
}
return
}
// Метод для обновления задачи
func (r *SQLiteRepository) TaskDone(taskId int) error {
if taskId == 0 { // Проверка на валидность
return errors.New("invalid updated ID")
}
// Поиск задачи по ее ID. Сначала создаем экземпляр ProjectTask
// передаем в него Task{ID: taskId}
pjTask := &ProjectTask{Task: Task{ID: taskId}}
tx := r.db.Find(&pjTask) // ищем задачу
if tx.Error != nil {
return tx.Error
}
pjTask.IsDone = true // обновляем поле IsDone
r.db.Save(&pjTask) // сохраняем обновленную задачу
// проверяем обновилась ли задача
rowsAffected := tx.RowsAffected
if rowsAffected == 0 {
return ErrUpdateFailed
}
return nil
}

View File

@@ -0,0 +1,21 @@
module golang/todo
go 1.24
require github.com/dixonwille/wmenu v4.0.2+incompatible
require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
golang.org/x/text v0.14.0 // indirect
)
require (
github.com/daviddengcn/go-colortext v1.0.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.24
golang.org/x/sys v0.6.0 // indirect
gopkg.in/dixonwille/wlog.v2 v2.0.0 // indirect
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.12
)

View File

@@ -0,0 +1,26 @@
github.com/daviddengcn/go-colortext v1.0.0 h1:ANqDyC0ys6qCSvuEK7l3g5RaehL/Xck9EX8ATG8oKsE=
github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c=
github.com/dixonwille/wmenu v4.0.2+incompatible h1:lxrPJsx9LpdUFD5T+dOfl6gPKLbBmiAtEdACLT1I2/w=
github.com/dixonwille/wmenu v4.0.2+incompatible/go.mod h1:DnajdZEKFQksxBctWekpWaQXQrDUHRBco6b8MyZnR1s=
github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho=
github.com/golangplus/bytes v1.0.0/go.mod h1:AdRaCFwmc/00ZzELMWb01soso6W1R/++O1XL80yAn+A=
github.com/golangplus/fmt v1.0.0/go.mod h1:zpM0OfbMCjPtd2qkTD/jX2MgiFCqklhSUFyDW44gVQE=
github.com/golangplus/testing v1.0.0/go.mod h1:ZDreixUV3YzhoVraIDyOzHrr76p6NUh6k/pPg/Q3gYA=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/dixonwille/wlog.v2 v2.0.0 h1:TbGWtD8ahWVSihKKr+z2Dw7Cv/7IrfN6dwrcrre17pU=
gopkg.in/dixonwille/wlog.v2 v2.0.0/go.mod h1:JYQHRnhGPLno/iATOiGkEXoRanJXqdz9Qo6/QwfARUc=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

View File

@@ -0,0 +1,16 @@
package main
import (
"golang/todo/db"
"golang/todo/menu"
"time"
)
func main() {
rep := db.NewSQLiteRepository()
defer rep.Close()
for {
menu.CreateMenu(rep)
time.Sleep(2 * time.Second)
}
}

View File

@@ -0,0 +1,90 @@
package menu
import (
"bufio"
"fmt"
"golang/todo/db"
"log"
"os"
"strconv"
"strings"
"github.com/dixonwille/wmenu"
)
func CreateMenu(rep *db.SQLiteRepository) {
menu := wmenu.NewMenu("What would you like to do?")
menu.Action(func(opts []wmenu.Opt) error { handleFunc(rep, opts); return nil })
menu.Option("Add a new Project", 0, false, nil)
menu.Option("Delete a Project by ID", 1, false, nil)
menu.Option("Get all Projects", 2, false, nil)
menu.Option("Add a Task", 3, false, nil)
menu.Option("Get all Tasks", 4, true, nil) // выбор по умолчанию
menu.Option("Get all Project tasks", 5, false, nil)
menu.Option("Done a Task by ID", 6, false, nil)
menu.Option("Delete a Task by ID", 7, false, nil)
menu.Option("Quit Application", 8, false, nil)
menuerr := menu.Run()
fmt.Println()
fmt.Println("---------------------------------")
if menuerr != nil {
log.Fatal(menuerr)
}
}
func handleFunc(rep *db.SQLiteRepository, opts []wmenu.Opt) {
switch opts[0].Value {
case 0:
fmt.Println("Adding a new Project")
addProject(rep)
case 1:
fmt.Println("Deleting a Project by ID")
deleteProjectByID(rep)
case 2:
fmt.Println("Getting all Projects")
getAllProjects(rep)
case 3:
fmt.Println("Adding a new Task")
addTask(rep)
case 4:
fmt.Println("Getting all Tasks")
getAllTasks(rep)
case 5:
fmt.Println("Getting all Project tasks by ProjectID")
getAllProjectTasks(rep)
case 6:
fmt.Println("Doing a Task by ID")
doneTask(rep)
case 7:
fmt.Println("Deleting a Task by ID")
deleteTaskByID(rep)
case 8:
fmt.Println("See you later!!!")
os.Exit(0)
}
}
func printNotValidData() {
fmt.Println("Data is not valid!!!")
}
func getIntValueFromStd(reader *bufio.Reader) (int, error) {
tempID, _, _ := reader.ReadLine()
idStr := strings.TrimSuffix(string(tempID), "\n")
idProj, err := strconv.Atoi(idStr)
if err != nil {
return 0, err
}
return idProj, nil
}
func getStringValueFromStd(reader *bufio.Reader) (string, error) {
data, err := reader.ReadString('\n')
data = strings.TrimSuffix(data, "\r\n")
if err != nil {
return "", err
}
return data, nil
}

View File

@@ -0,0 +1,65 @@
package menu
import (
"bufio"
"fmt"
"golang/todo/db"
"os"
)
func addProject(rep *db.SQLiteRepository) {
project := db.Project{}
reader := bufio.NewReader(os.Stdin)
fmt.Print("Input project name: ")
name, _ := getStringValueFromStd(reader)
fmt.Print("Input description project: ")
desc, _ := getStringValueFromStd(reader)
project.Name = name
project.Description = desc
if project.Name != "" && project.Description != "" {
project, err := rep.AddProject(project)
if err != nil {
fmt.Println(err)
} else {
fmt.Printf("\nAdded project: %+v\n", *project)
}
} else {
printNotValidData()
}
}
func deleteProjectByID(rep *db.SQLiteRepository) {
fmt.Print("Input ID for deleting project: ")
id, err := getIntValueFromStd(bufio.NewReader(os.Stdin))
if err != nil {
printNotValidData()
return
}
err = rep.DeleteProject(id)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("Project deleted")
}
func getAllProjects(rep *db.SQLiteRepository) {
progects, err := rep.GetAllProjects()
if err != nil {
printNotValidData()
return
}
if len(progects) == 0 {
fmt.Println("You don't have any project")
} else {
fmt.Println("You current projects:")
for _, it := range progects {
fmt.Printf("ProjectID: %v || Name: %v || Desc: %v\n",
it.ID, it.Name, it.Description)
}
}
}

View File

@@ -0,0 +1,122 @@
package menu
import (
"bufio"
"fmt"
"golang/todo/db"
"os"
)
func addTask(rep *db.SQLiteRepository) {
task := db.Task{}
reader := bufio.NewReader(os.Stdin)
fmt.Print("Input project ID: ")
projectID, err := getIntValueFromStd(reader)
if err != nil {
printNotValidData()
return
}
fmt.Print("Input task name: ")
name, _ := getStringValueFromStd(reader)
fmt.Print("Input description task: ")
desc, _ := getStringValueFromStd(reader)
fmt.Print("Input priority task: ")
priority, err := getIntValueFromStd(reader)
if err != nil {
printNotValidData()
return
}
task.Name = name
task.Description = desc
task.Priority = uint8(priority)
if task.Name != "" && task.Description != "" {
task, err := rep.AddTask(task, projectID)
if err != nil {
fmt.Println(err)
}
fmt.Printf("\nAdded task: %+v\n", *task)
} else {
printNotValidData()
}
}
func deleteTaskByID(rep *db.SQLiteRepository) {
fmt.Print("Input ID for deleting task: ")
id, err := getIntValueFromStd(bufio.NewReader(os.Stdin))
if err != nil {
printNotValidData()
return
}
err = rep.DeleteTask(id)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("Task deleted")
}
func getAllTasks(rep *db.SQLiteRepository) {
tasks, err := rep.GetAllTasks()
if err != nil {
printNotValidData()
return
}
if len(tasks) == 0 {
fmt.Println("You don't have any task")
} else {
fmt.Println("You current tasks: ")
for _, it := range tasks {
fmt.Printf("TaskID: %v || Name: %v || Desc: %v || Priority: %v || IsDone: %v || ProjID: %v\n",
it.ID, it.Name, it.Description, it.Priority, it.IsDone, it.ProjectID)
}
}
}
func getAllProjectTasks(rep *db.SQLiteRepository) {
fmt.Print("Input ID for project: ")
id, err := getIntValueFromStd(bufio.NewReader(os.Stdin))
if err != nil {
printNotValidData()
return
}
tasks, err := rep.GetProjectTasks(id)
if err != nil {
fmt.Println(err)
return
}
if len(tasks) == 0 {
fmt.Println("You don't have any task")
} else {
fmt.Printf("Project with ID = %d have next tasks:\n", id)
for _, it := range tasks {
fmt.Printf("TaskID: %v || Name: %v || Desc: %v || Priority: %v || IsDone: %v\n",
it.ID, it.Name, it.Description, it.Priority, it.IsDone)
}
}
}
func doneTask(rep *db.SQLiteRepository) {
fmt.Print("Input task ID: ")
id, err := getIntValueFromStd(bufio.NewReader(os.Stdin))
if err != nil {
printNotValidData()
return
}
err = rep.TaskDone(id)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("Congratulations! Task done!")
}

Binary file not shown.

View File

@@ -0,0 +1,14 @@
module golang/todo
go 1.24
require gorm.io/gorm v1.25.12
require github.com/mattn/go-sqlite3 v1.14.22 // indirect
require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
golang.org/x/text v0.14.0 // indirect
gorm.io/driver/sqlite v1.5.7
)

View File

@@ -0,0 +1,12 @@
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

View File

@@ -0,0 +1,243 @@
package main
import (
"errors"
"fmt"
"os"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
const dbName = "todo.db"
var ErrNotExists = errors.New("row not exists")
type Project struct {
ID int `gorm:"primary_key;autoIncrement:true;not null"`
Name string `gorm:"unique;not null"`
Description string
}
type ProjectTask struct {
ID int `gorm:"primary_key;autoIncrement;not null"`
Name string `gorm:"not null"`
Description string `gorm:"not null"`
Priority uint8 `gorm:"not null"`
IsDone bool `gorm:"not null"`
ProjectID int `gorm:"not null"`
Project *Project `gorm:"foreignKey:ProjectID;references:ID"`
}
func connectionToBD(pathToDB string) (db *gorm.DB, err error) {
if _, err := os.Stat(dbName); os.IsNotExist(err) {
db, err = gorm.Open(sqlite.Open(dbName), &gorm.Config{})
if err != nil {
return nil, err
}
fmt.Println("DB isn't exist")
db.AutoMigrate(&Project{}, &ProjectTask{})
putDefaultValuesToDB(db)
} else {
db, err = gorm.Open(sqlite.Open(dbName), &gorm.Config{})
if err != nil {
return nil, err
}
fmt.Println("DB already exists")
}
return
}
func putDefaultValuesToDB(db *gorm.DB) {
db.Transaction(func(tx *gorm.DB) error { // начало транзакции
firstProject := Project{
Name: "Go",
Description: "Roadmap for learning Go",
}
secondProject := Project{
Name: "One Year",
Description: "Tasks for the year",
}
tx.Create(&firstProject)
if err := tx.Create(&secondProject).Error; err != nil { //проверяем на наличие ошибок при записи
return err // вызываем отмену транзакции
}
tx.Create(&ProjectTask{
Name: "Variable",
Description: "Learning Go build-in variables",
Priority: 1,
Project: &firstProject,
})
if err := tx.Create(&ProjectTask{ //проверяем на наличие ошибок при записи
Name: "Struct",
Description: "Learning use struct in OOP code",
Priority: 3,
Project: &firstProject,
}).Error; err != nil {
return err // вызываем отмену транзакции
}
tx.Create(&ProjectTask{
Name: "Goroutine",
Description: "Learning concurrent programming",
Priority: 5,
Project: &firstProject,
})
tx.Create(&ProjectTask{
Name: "DataBase",
Description: "How write app with db",
Priority: 1,
Project: &firstProject,
})
tx.Create(&ProjectTask{
Name: "PhD",
Description: "Ph.D. in Technical Sciences",
Priority: 5,
Project: &secondProject,
})
tx.Create(&ProjectTask{
Name: "Losing weight",
Description: "Exercise and eat less chocolate",
Priority: 2,
Project: &secondProject,
})
tx.Create(&ProjectTask{
Name: "Пафос и превозмогание",
Description: "10к подписчиков на канале",
Priority: 2,
Project: &secondProject,
})
return nil
})
}
func GetAllProjects(db *gorm.DB) ([]Project, error) {
var projects []Project
tx := db.Find(&projects)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, ErrNotExists
}
return projects, nil
}
func GetAllTasks(db *gorm.DB) (tasks []ProjectTask, err error) {
tx := db.Find(&tasks)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, ErrNotExists
}
return
}
func printAllTasks(db *gorm.DB) {
tasks, err := GetAllTasks(db)
if err != nil {
return
}
fmt.Println("*********Tasks*********")
for _, it := range tasks {
fmt.Printf("TaskID: %v || Name: %v || Priority: %v || IsDone: %v || ProjID: %v\n",
it.ID, it.Name, it.Priority, it.IsDone, it.ProjectID)
}
}
func printAllProjects(db *gorm.DB) {
progects, err := GetAllProjects(db)
if err != nil {
return
}
fmt.Println("*********Projects*********")
for _, it := range progects {
fmt.Printf("ProjectID: %v || Name: %v || Desc: %v\n",
it.ID, it.Name, it.Description)
}
}
func printAllProjectAndTask(db *gorm.DB) {
printAllProjects(db)
printAllTasks(db)
}
func GetAllProjectTasks(db *gorm.DB, projectID int) (tasks []ProjectTask, err error) {
tx := db.Where("project_id", projectID).Find(&tasks)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, ErrNotExists
}
return
}
func printAllProjectTasks(db *gorm.DB, projectID int) {
tasks, err := GetAllProjectTasks(db, projectID)
if err != nil {
return
}
fmt.Println("*********Tasks*********")
for _, it := range tasks {
fmt.Printf("TaskID: %v || Name: %v || Priority: %v || IsDone: %v || ProjID: %v\n",
it.ID, it.Name, it.Priority, it.IsDone, it.ProjectID)
}
}
func main() {
db, _ := gorm.Open(sqlite.Open(dbName), &gorm.Config{DryRun: true})
var tasks []ProjectTask
stmt := db.Where("project_id", 1).Find(&tasks).Statement
fmt.Println(stmt.SQL.String())
stmt = db.Create(&ProjectTask{
Name: "Пафос и превозмогание",
Description: "10к подписчиков на канале",
Priority: 2,
ProjectID: 2,
}).Statement
fmt.Println(stmt.SQL.String())
firstProject := Project{
Name: "Go",
Description: "Roadmap for learning Go",
}
stmt = db.Create(&firstProject).Statement
fmt.Println(stmt.SQL.String())
}
func CreateProjects(db *gorm.DB) error {
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := tx.Error; err != nil {
return err
}
if err := tx.Create(&Project{Name: "Oo"}).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Create(&Project{Name: "^_^"}).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
}

Binary file not shown.

14
part_6/tic_tac_toe/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}", // <- ставим запятую
"console": "integratedTerminal"
}
]
}

View File

@@ -0,0 +1,113 @@
package game
import (
"fmt"
)
const (
BoardDefaultSize int = 3
BoardMinSize int = 3
BoardMaxSize int = 9
)
type Board struct {
Board [][]BoardField `json:"board"`
Size int `json:"size"`
}
func NewBoard(size int) *Board {
board := make([][]BoardField, size)
for i := range board {
board[i] = make([]BoardField, size)
}
return &Board{Board: board, Size: size}
}
// Отображение игрового поля
func (b *Board) printBoard() {
fmt.Print(" ")
for i := range b.Size {
fmt.Printf("%d ", i+1)
}
fmt.Println()
for i := range b.Size {
fmt.Printf("%d ", i+1)
for j := range b.Size {
switch b.Board[i][j] {
case empty:
fmt.Print(". ")
case cross:
fmt.Print("X ")
case nought:
fmt.Print("O ")
}
}
fmt.Println()
}
}
// Проверка возможности и выполнения хода
func (b *Board) makeMove(x, y int) bool {
return b.Board[x][y] == empty
}
func (b *Board) setSymbol(x, y int, player BoardField) bool {
if b.makeMove(x, y) {
b.Board[x][y] = player
return true
}
return false
}
// Проверка выигрыша
func (b *Board) checkWin(player BoardField) bool {
// Проверка строк и столбцов
for i := range b.Size {
rowWin, colWin := true, true
for j := range b.Size {
if b.Board[i][j] != player {
rowWin = false
}
if b.Board[j][i] != player {
colWin = false
}
}
if rowWin || colWin {
return true
}
}
// Главная диагональ
mainDiag := true
for i := range b.Size {
if b.Board[i][i] != player {
mainDiag = false
break
}
}
if mainDiag {
return true
}
// Побочная диагональ
antiDiag := true
for i := range b.Size {
if b.Board[i][b.Size-i-1] != player {
antiDiag = false
break
}
}
return antiDiag
}
// Проверка на ничью
func (b *Board) checkDraw() bool {
for i := range b.Size {
for j := range b.Size {
if b.Board[i][j] == empty {
return false
}
}
}
return true
}

View File

@@ -0,0 +1,10 @@
package game
type BoardField int
// фигуры в клетке поля
const (
empty BoardField = iota
cross
nought
)

View File

@@ -0,0 +1,122 @@
package game
import (
"bufio"
"fmt"
"strconv"
"strings"
)
type Game struct {
Board *Board `json:"board"`
Player *Player `json:"player"`
Reader *bufio.Reader `json:"-"`
State GameState `json:"state"`
Saver IGameSaver `json:"-"`
}
func NewGame(board Board, player Player,
reader *bufio.Reader, saver IGameSaver) *Game {
return &Game{
Board: &board,
Player: &player,
Reader: reader,
State: playing,
Saver: saver,
}
}
func (g *Game) updateState() {
if g.Board.checkWin(g.Player.Figure) {
if g.Player.Figure == cross {
g.State = crossWin
} else {
g.State = noughtWin
}
} else if g.Board.checkDraw() {
g.State = draw
}
}
func (g *Game) saveCheck(input string) bool {
if input == "save" {
fmt.Println("Enter file name: ")
fileName, err := g.Reader.ReadString('\n')
if err != nil {
fmt.Println("Invalid input. Please try again.")
return false
}
fileName = strings.TrimSpace(fileName)
err = g.Saver.SaveGame(fileName, g)
if err != nil {
fmt.Println("Error saving game.")
return false
}
fmt.Println("Game saved successfully!!!")
return true
}
return false
}
// Игровой цикл
func (g *Game) Play() {
for g.State == playing {
g.Board.printBoard()
fmt.Printf(
"%s's turn. Enter row and column (e.g. 1 2): ",
g.Player.getSymbol())
input, err := g.Reader.ReadString('\n')
input = strings.TrimSpace(input)
if err != nil {
fmt.Println("Invalid input. Please try again.")
continue
}
input = strings.TrimSpace(input)
if input == "q" {
g.State = quit
break
}
if g.saveCheck(input) {
continue
}
parts := strings.Fields(input)
if len(parts) != 2 {
fmt.Println("Invalid input. Please try again.")
continue
}
row, err1 := strconv.Atoi(parts[0])
col, err2 := strconv.Atoi(parts[1])
if err1 != nil || err2 != nil ||
row < 1 || col < 1 || row > g.Board.Size ||
col > g.Board.Size {
fmt.Println("Invalid input. Please try again.")
continue
}
if g.Board.setSymbol(row-1, col-1, g.Player.Figure) {
g.updateState()
g.Player.switchPlayer()
} else {
fmt.Println("This cell is already occupied!")
}
}
g.Board.printBoard()
if g.State == crossWin {
fmt.Println("X wins!")
} else if g.State == noughtWin {
fmt.Println("O wins!")
} else if g.State == draw {
fmt.Println("It's a draw!")
} else {
fmt.Println("Game over!")
}
}

View File

@@ -0,0 +1,12 @@
package game
type GameState int
// состояние игрового процесса
const (
playing GameState = iota
draw
crossWin
noughtWin
quit
)

View File

@@ -0,0 +1,24 @@
package game
type Player struct {
Figure BoardField `json:"figure"`
}
func NewPlayer() *Player {
return &Player{Figure: cross}
}
func (p *Player) switchPlayer() {
if p.Figure == cross {
p.Figure = nought
} else {
p.Figure = cross
}
}
func (p *Player) getSymbol() string {
if p.Figure == cross {
return "X"
}
return "O"
}

View File

@@ -0,0 +1,3 @@
module tic-tac-toe
go 1.24.0

View File

@@ -0,0 +1 @@
{"board":{"board":[[0,2,0,0],[0,0,1,0],[0,0,0,0],[0,0,1,0]],"size":4},"player":{"figure":2},"state":0}