Drakirus/Sudoku

View on GitHub
class/board_class.rb

Summary

Maintainability
B
6 hrs
Test Coverage
#!/usr/bin/env ruby

# encoding: UTF-8

##
# Author:: CHAMPION Pierre
# License:: MIT Licence
#
# https://github.com/Drakirus/Sudoku
#

#:nodoc:
require_relative './cell_class.rb'
require 'yaml'

# === Gestion de cases dans une planche de Sudoku
# === Variables d'instance
# - +rows+      -> (*PRIVATE*) Hash des lignes<br>
# - +columns+ -> (*PRIVATE*) Hash des colonnes<br>
# - +boxes+        -> (*PRIVATE*) Hash des "familles" des boxes<br>
# - +cells+      -> (*PRIVATE*) Tableau des valeurs des Cell
#
class Board
  attr_accessor :difficulty, :time
  attr_reader :bUseSolution, :bMakeError
  def initialize(numbers)#:nodoc:
    # Hash
    @rows = {}
    @columns = {}
    @boxes = {}
    # Tableau
    @cells = []
    @time = 0
    @bUseSolution = false
    @bMakeError = false


    # parcourir les cases du Sudoku
    9.times do |row|

      9.times do |col|

        # le num de la box
        box= (3 * (row / 3)) + (col / 3)

        cell = Cell.creer(row, col, box)

        # valeur initial
        if numbers==nil
          cell.value = 0
        else
          cell.value = numbers.pop
        end

        # Met des tableaux dans les hashs (INIT)
        @rows[row] = [] if !@rows.has_key?(row)
        @columns[col] = [] if !@columns.has_key?(col)
        @boxes[box] = [] if !@boxes.has_key?(box)

        # Ajout des cases dans les conteneurs
        @rows[row] << cell
        @columns[col] << cell
        @boxes[box] << cell
        @cells << cell

      end
    end
  end

  # Représentation d'une planche
  # * *Arguments*    :
  #   - +numbers+ -> Liste des numbre a mettre dans la planche
  # * *Returns*
  #   - Board
  # * Exemple
  #    planche_base="
  #    0 2 3   4 5 6   7 8 9
  #    4 5 6   7 8 9   1 2 3
  #    7 8 9   1 2 3   4 5 6
  #
  #    2 1 4   3 6 5   8 9 7
  #    3 6 5   8 9 7   2 1 4
  #    8 9 7   2 1 4   3 6 5
  #
  #    5 3 1   6 4 2   9 7 8
  #    6 4 2   9 7 8   5 3 1
  #    9 7 8   5 3 1   6 4 2
  #    "
  #    x = Board.creer(planche_base.delete("\s|\n")
  #      .split("").reverse.map(&:to_i))
  #    print x
  def self.creer(numbers=nil)
    new(numbers)
  end
  private_class_method :new


  include Comparable
  # Méthode de comparaison entre différente instance de Board
  # * *Arguments*    :
  #   - +other+ -> autre Board a compare
  # * *Returns*
  #   - true/false
  def ==(other)
    each_with_coord do |cell,i,j|
      if cell!=other.cellAt(i,j)
        return false
      end
    end
    return true
  end

  # Obtenir la Cell a la place coordonne
  # * *Arguments*    :
  #   - +i+ -> la ligne
  #   - +j+ -> la colonne
  # * *Returns*
  #   - Cell
  def cellAt(i,j)
      @rows[i][j]
  end

  # iterateur qui loop sur les cases
  # avec comme information la case, ligne, colonne, boxe
  def each_with_coord
    9.times do |i|
      9.times do |j|
        yield @rows[i][j], i, j, @rows[i][j].box
      end
    end
  end

  # Obtenir les Cell qui sont défini.
  # * *Returns*
  #   - Array of Cell
  def usedCells
    @cells.inject([]) do |result, cell|
      result << cell if not cell.vide?
      result
    end
  end

  # Obtenir les Cell qui sont vide.
  # * *Returns*
  #   - Array of Cell
  def unusedCells
    @cells.inject([]) do |result, cell|
      result << cell if cell.vide?
      result
    end
  end

  # Obtenir les Cell de meme value dans la planche
  # * *Arguments*
  #   - +Cell+ -> cell (Valeur)
  # * *Returns*
  #   - Array of Cell
  def sameCellsValue(cellArg)
    @cells.inject([]) do |result, cell|
      result << cell if cell.value == cellArg.value
      result
    end
  end

  # Retourne toutes les Cell sur la meme *ligne* *colonne* et *boxe* d'une Cell.
  # * *Arguments*    :
  #   - +cell+ -> Cell du puzzle
  # * *Returns*
  #   - Array of Cell
  def linked(cell)
    @rows[cell.row] + @columns[cell.col] + @boxes[cell.box]
  end

  # Retourne toutes les valeurs qu'une Cell *Ne-peut-pas* prendre.<br>
  # *Valeur-de-la-Cell-exclu*
  # * *Arguments*    :
  #   - +cell+ -> Cell du puzzle
  # * *Returns*
  #   - Array of numbers
  def excluded(cell)
    excluded = linked(cell).inject([]) do |result, cellloop|
      result << cellloop.value if not cellloop.vide?
      result
    end
    excluded.uniq - [cell.value]
  end

  # Retourne toutes les valeurs qu'une Cell peut prendre.
  # * *Arguments*    :
  #   - +cell+ -> Cell du puzzle
  # * *Returns*
  #   - Array of numbers
  def possibles(cell)
    ((1..9).to_a - excluded(cell))
  end

  # Echange 2 lignes d'une planche.
  # * *Arguments*    :
  #   - +index_row1+ -> num de la ligne
  #   - +index_row2+ -> num de la ligne2
  #   - +force+ -> si true autorise les swap entre boxes {rdoc-image:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAA8FBMVEX//////wCQkJAwMDBxcQAAAADe3gD9/QDT09OsrKzz8/P6+gD//zGpqans7AD//1X6+vbm5r36+nvW1gBlZRkrKzOCgoLs7JhiYmMpKSLr69uIiIj//yP//3xZWVne3q8qKi2qqgFsbC0aGhpTUycbGw9raxfy8prz8+je3rhkZAAYGBze3qnPzxEFBR1ZWSbAwADU1Iizsz5+fgHt7VE/PxnT08ZDQ0NwcHDHx8efnwC3txSRkQPCwlrz8zJMTFB3dxzl5eU3Nz/S0lH//0uQkD3Y2Gm9vT2Dg10yMgAAAAyjo2lTUw08PAAKCgCBgVBOcdPBAAABFklEQVQokW2Qf1eCMBSGd61JtAElFbShQWaFRFFoomVlpaX9+v7fJjY0Hfb8s5295z33uUNoARuO++gfmPP68lwZrQdhjZpPvrGWsDNLM4PDhr6tvu/s1igQe2//YEsNzh1LAzCDlmuolaM6fUgSah83uVJhjuU94tkPCXx1SlinBGM8BfvUX60IJfKN8ReoYoWSbKhiUkn7xDgtieVKANoHnk0gryzFmNwBqq2pOMylWCgKALT3LgLyJyaURAADWx4LMakk8N7SDshKIVb8Uk58iU/mFSl2UUxYCUh8FeVTri0PNIF3k3bkhSTtZmagoUeqBeb8Et93o4wjdjfYVGl3I9flCN1mvY0SDTfTxSb9SgnO9dEv8K0fkCJzF2cAAAAASUVORK5CYII=}[http://coucou/]
  # * *Returns*
  #   - Board
  def swapRow(index_row1, index_row2, force=false)
    swap(index_row1, index_row2, @rows, force)
  end


  # Echange 2 colonnes d'une planche.
  # * *Arguments*    :
  #   - +index_columns1+ -> num de la colonne
  #   - +index_columns2+ -> num de la colonne2
  #   - +force+ -> si true autorise les swap entre boxes {rdoc-image:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAA8FBMVEX//////wCQkJAwMDBxcQAAAADe3gD9/QDT09OsrKzz8/P6+gD//zGpqans7AD//1X6+vbm5r36+nvW1gBlZRkrKzOCgoLs7JhiYmMpKSLr69uIiIj//yP//3xZWVne3q8qKi2qqgFsbC0aGhpTUycbGw9raxfy8prz8+je3rhkZAAYGBze3qnPzxEFBR1ZWSbAwADU1Iizsz5+fgHt7VE/PxnT08ZDQ0NwcHDHx8efnwC3txSRkQPCwlrz8zJMTFB3dxzl5eU3Nz/S0lH//0uQkD3Y2Gm9vT2Dg10yMgAAAAyjo2lTUw08PAAKCgCBgVBOcdPBAAABFklEQVQokW2Qf1eCMBSGd61JtAElFbShQWaFRFFoomVlpaX9+v7fJjY0Hfb8s5295z33uUNoARuO++gfmPP68lwZrQdhjZpPvrGWsDNLM4PDhr6tvu/s1igQe2//YEsNzh1LAzCDlmuolaM6fUgSah83uVJhjuU94tkPCXx1SlinBGM8BfvUX60IJfKN8ReoYoWSbKhiUkn7xDgtieVKANoHnk0gryzFmNwBqq2pOMylWCgKALT3LgLyJyaURAADWx4LMakk8N7SDshKIVb8Uk58iU/mFSl2UUxYCUh8FeVTri0PNIF3k3bkhSTtZmagoUeqBeb8Et93o4wjdjfYVGl3I9flCN1mvY0SDTfTxSb9SgnO9dEv8K0fkCJzF2cAAAAASUVORK5CYII=}[http://coucou/]
  # * *Returns*
  #   - Board
  def swapColumns(index_columns1, index_columns2, force=false)
    swap(index_columns1, index_columns2, @columns, force)
  end

  # Echange 2 conteneur d'une planche.
  # * *Arguments*    :
  #   - +index_columns1+ -> num de la index
  #   - +index_columns2+ -> num de la index
  #   - +force+ -> si true autorise les swap entre boxes {rdoc-image:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAA8FBMVEX//////wCQkJAwMDBxcQAAAADe3gD9/QDT09OsrKzz8/P6+gD//zGpqans7AD//1X6+vbm5r36+nvW1gBlZRkrKzOCgoLs7JhiYmMpKSLr69uIiIj//yP//3xZWVne3q8qKi2qqgFsbC0aGhpTUycbGw9raxfy8prz8+je3rhkZAAYGBze3qnPzxEFBR1ZWSbAwADU1Iizsz5+fgHt7VE/PxnT08ZDQ0NwcHDHx8efnwC3txSRkQPCwlrz8zJMTFB3dxzl5eU3Nz/S0lH//0uQkD3Y2Gm9vT2Dg10yMgAAAAyjo2lTUw08PAAKCgCBgVBOcdPBAAABFklEQVQokW2Qf1eCMBSGd61JtAElFbShQWaFRFFoomVlpaX9+v7fJjY0Hfb8s5295z33uUNoARuO++gfmPP68lwZrQdhjZpPvrGWsDNLM4PDhr6tvu/s1igQe2//YEsNzh1LAzCDlmuolaM6fUgSah83uVJhjuU94tkPCXx1SlinBGM8BfvUX60IJfKN8ReoYoWSbKhiUkn7xDgtieVKANoHnk0gryzFmNwBqq2pOMylWCgKALT3LgLyJyaURAADWx4LMakk8N7SDshKIVb8Uk58iU/mFSl2UUxYCUh8FeVTri0PNIF3k3bkhSTtZmagoUeqBeb8Et93o4wjdjfYVGl3I9flCN1mvY0SDTfTxSb9SgnO9dEv8K0fkCJzF2cAAAAASUVORK5CYII=}[http://coucou/]
  #   - +conteneur+ -> @rows, @columns
  # * *Returns*
  #   - Board
  private def swap(index1, index2, conteneur, force=false)
    if index1 / 3 != index2 / 3 and force == false
      raise "Tried to swap non-boxed #{conteneur.class}"
    end
    for x in (0..(conteneur[index2]).length-1) do
      temp = conteneur[index1][x].value
      conteneur[index1][x].value = conteneur[index2][x].value
      conteneur[index2][x].value = temp
    end
    return self
  end

  # Echange 2 groupe de colonnes d'une planche.
  # * *Arguments*    :
  #   - +index1+  -> num du groupe de colonne1
  #   - +index2+  -> num du groupe de colonne2
  # * *Returns*
  #   - Board
  def swapStack(index1, index2)
    boxesLoop{ |v| swapColumns(index1 * 3 + v, index2 * 3 + v, true) }
  end

  # Echange 2 groupe de lignes d'une planche.
  # * *Arguments*    :
  #   - +index1+  -> num du groupe de ligne1
  #   - +index2+  -> num du groupe de ligne2
  # * *Returns*
  #   - Board
  def swapBand(index1, index2)
    boxesLoop{ |v| swapRow(index1 * 3 + v, index2 * 3 + v, true) }
  end

  # Permet de parcourir les boxes. DRY
  private def boxesLoop
    3.times do |v|
      yield v
    end
  end

  # Copie la planche.
  # * *Returns*
  #   - Board (indépendant)
  def copie
    values = []
    9.times do |index|
      @rows[index].each do |cell|
        values << cell.value
      end
    end
    res = Board.creer(values.reverse)
    self.each_with_coord do |cell,i,j|
      if cell.freeze?
        res.cellAt(i,j).freeze
      end
    end
    return res
  end


  # Vérifie que la planche est valide.
  # * *Returns*
  #   - true/false
  def valid?
    valid = (0..9).to_a
    # parcourir tours le conteneurs
    [@rows, @columns, @boxes].each do |set|
      # pour chaque lignes, colonnes, boxes
      9.times do |index|
        # creation d'un tableau des valeurs
        values = []
        set[index].each do |cell|
          values << cell.value
        end

        # une case de meme valeur par set (oublie le 0)
        if not (values.select { |e| values.count(e) > 1 } - [0]).empty?
          return false
        end
        # Touts les chiffre sont des chiffre possibles
        if not (values - valid).length == 0
          return false
        end
      end
    end
    true
  end

  # Vérifie que la planche est Fini et valide (VICTOIRE).
  # * *Returns*
  #   - true/false
  def complete?
     unusedCells.length == 0 and valid?
  end

  # Une fois la grille générer les valeurs pas défaut sont inchantable
  def freeze
    each_with_coord do |cell|
      cell.freeze if not cell.vide?
    end
    return self
  end

  # création d'une représentation en chaine de caractères.
  # * Exemple
  #    1 2 3   4 5 6   7 8 9
  #    4 5 6   7 8 9   1 2 3
  #    7 8 9   1 2 3   4 5 6
  #
  #    2 1 4   3 6 5   8 9 7
  #    3 6 5   8 9 7   2 1 4
  #    8 9 7   2 1 4   3 6 5
  #
  #    5 3 1   6 4 2   9 7 8
  #    6 4 2   9 7 8   5 3 1
  #    9 7 8   5 3 1   6 4 2
  def to_s
    output = "\n"
    9.times do |index|
      @rows[index].each_with_index do |cell, indextab|
        output += "  " if indextab % 3 == 0
        if cell.value != 0
          output += cell.value.to_s
        else
          output += "_"
        end
        output +=  " "
      end
      output += "\n"
      output += "\n" if index % 3 == 2
    end
    return output
  end

  # Serialize une planche.
  # * *Arguments*    :
  #   - +nameFic+  -> le nom du fichier pour la sauvegarde
  def serialized(nameFic)
    # file = File.open(nameFic, "w+")
    # puts("Coucou je serialize")
    # jsonSave = YAML::dump(self)
    # file.write(jsonSave)
    # file.close

    File.open(nameFic, "w+") do |f|
      # Marshal.dump(self, f)
      YAML.dump(self, f)
    end
  end

  # Serialize une planche.
  # * *Arguments*    :
  #   - +nameFic+  -> le nom du fichier pour la sauvegarde
  # * *Returns*      :
  #   - le fichier chargé et convertit en langage machine
  def self.unserialized(nameFic)
    # file = File.open(nameFic, "r")
    # jsonSave = file.read
    # data = JSON.parse(json)
    # self = data
    return YAML.load_file(nameFic)
    # return Marshal.load(File.read(nameFic))
  end

  # tabs des snapshot
  @@images_undo = []
  @@images_redo = []

  # new branch in undo history
  @@newbrach = true

  # Serialize une planche dans une file de snapshot
  # permet undo redo
  def snapshot
    if not @@images_redo.empty? and @@newbrach
      @@images_undo << @@images_redo.last
      @@newbrach = false
    end
    @@images_undo << Marshal.dump(self)
    return @@images_undo.length
  end

  # get last undo
  def retrive_undo
    if not @@images_undo.empty?
      img = @@images_undo.pop
      board = Marshal.load(img)
      @@images_redo << img
      @@newbrach = true
      if board == self
        board = retrive_undo
      end
      return board
    else
      print "end undo list\n"
      self
    end
  end

  # get last redo
  def retrive_redo
    if not @@images_redo.empty?
      img = @@images_redo.pop
      board = Marshal.load(img)
      @@images_undo << img
      if board == self
        board = retrive_redo
      end
      return board
    else
      print "end redo list\n"
      self
    end
  end

  def hasUseSolution
    @bUseSolution = true
  end

  def hasMakeError
    @bMakeError = true
  end
end